From 3421e33554ead922bae8892ddd4bff4471873ff9 Mon Sep 17 00:00:00 2001 From: =?utf8?q?F=C3=A9lix=20Sipma?= Date: Mon, 31 Oct 2016 12:28:53 +0000 Subject: [PATCH] Import patat_0.3.3.0.orig.tar.gz [dgit import orig patat_0.3.3.0.orig.tar.gz] --- .gitignore | 2 + .travis.yml | 7 + CHANGELOG.md | 24 ++ LICENSE | 339 +++++++++++++++ README.md | 290 +++++++++++++ Setup.hs | 2 + debian/changelog | 5 + debian/compat | 1 + debian/control | 40 ++ debian/copyright | 29 ++ debian/docs | 1 + debian/lintian-overrides | 2 + debian/patat.examples | 1 + debian/patat.install | 1 + debian/patat.manpages | 1 + debian/patat.md | 48 +++ debian/patches/0001-Debianize-README.md.patch | 78 ++++ debian/patches/0002-add-version.patch | 95 ++++ debian/patches/series | 2 + debian/rules | 26 ++ debian/source/format | 1 + debian/watch | 4 + extra/screenshot.png | Bin 0 -> 34652 bytes patat.cabal | 57 +++ src/Data/Aeson/Extended.hs | 22 + src/Data/Aeson/TH/Extended.hs | 21 + src/Data/Data/Extended.hs | 23 + src/Main.hs | 170 ++++++++ src/Patat/Presentation.hs | 20 + src/Patat/Presentation/Display.hs | 311 ++++++++++++++ src/Patat/Presentation/Display/CodeBlock.hs | 79 ++++ src/Patat/Presentation/Display/Table.hs | 107 +++++ src/Patat/Presentation/Interactive.hs | 100 +++++ src/Patat/Presentation/Internal.hs | 71 +++ src/Patat/Presentation/Read.hs | 121 ++++++ src/Patat/PrettyPrint.hs | 404 ++++++++++++++++++ src/Patat/Theme.hs | 286 +++++++++++++ src/Text/Pandoc/Extended.hs | 50 +++ stack.yaml | 6 + test.sh | 30 ++ tests/01.md | 14 + tests/01.md.dump | 8 + tests/02.lhs | 6 + tests/02.lhs.dump | 8 + tests/03.md | 46 ++ tests/03.md.dump | 48 +++ tests/deflist.md | 20 + tests/deflist.md.dump | 24 ++ tests/links.md | 8 + tests/links.md.dump | 10 + tests/lists.md | 13 + tests/lists.md.dump | 15 + tests/syntax.md | 14 + tests/syntax.md.dump | 7 + tests/tables.md | 48 +++ tests/tables.md.dump | 48 +++ tests/themes.md | 11 + tests/themes.md.dump | 5 + tests/wrapping.md | 23 + tests/wrapping.md.dump | 17 + 60 files changed, 3270 insertions(+) create mode 100644 .gitignore create mode 100644 .travis.yml create mode 100644 CHANGELOG.md create mode 100644 LICENSE create mode 100644 README.md create mode 100644 Setup.hs create mode 100644 debian/changelog create mode 100644 debian/compat create mode 100644 debian/control create mode 100644 debian/copyright create mode 100644 debian/docs create mode 100644 debian/lintian-overrides create mode 100644 debian/patat.examples create mode 100644 debian/patat.install create mode 100644 debian/patat.manpages create mode 100644 debian/patat.md create mode 100644 debian/patches/0001-Debianize-README.md.patch create mode 100644 debian/patches/0002-add-version.patch create mode 100644 debian/patches/series create mode 100755 debian/rules create mode 100644 debian/source/format create mode 100644 debian/watch create mode 100644 extra/screenshot.png create mode 100644 patat.cabal create mode 100644 src/Data/Aeson/Extended.hs create mode 100644 src/Data/Aeson/TH/Extended.hs create mode 100644 src/Data/Data/Extended.hs create mode 100644 src/Main.hs create mode 100644 src/Patat/Presentation.hs create mode 100644 src/Patat/Presentation/Display.hs create mode 100644 src/Patat/Presentation/Display/CodeBlock.hs create mode 100644 src/Patat/Presentation/Display/Table.hs create mode 100644 src/Patat/Presentation/Interactive.hs create mode 100644 src/Patat/Presentation/Internal.hs create mode 100644 src/Patat/Presentation/Read.hs create mode 100644 src/Patat/PrettyPrint.hs create mode 100644 src/Patat/Theme.hs create mode 100644 src/Text/Pandoc/Extended.hs create mode 100644 stack.yaml create mode 100644 test.sh create mode 100644 tests/01.md create mode 100644 tests/01.md.dump create mode 100644 tests/02.lhs create mode 100644 tests/02.lhs.dump create mode 100644 tests/03.md create mode 100644 tests/03.md.dump create mode 100644 tests/deflist.md create mode 100644 tests/deflist.md.dump create mode 100644 tests/links.md create mode 100644 tests/links.md.dump create mode 100644 tests/lists.md create mode 100644 tests/lists.md.dump create mode 100644 tests/syntax.md create mode 100644 tests/syntax.md.dump create mode 100644 tests/tables.md create mode 100644 tests/tables.md.dump create mode 100644 tests/themes.md create mode 100644 tests/themes.md.dump create mode 100644 tests/wrapping.md create mode 100644 tests/wrapping.md.dump diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1a4350c --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +dist +.stack-work diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..f1fac0b --- /dev/null +++ b/.travis.yml @@ -0,0 +1,7 @@ +language: haskell +ghc: '7.8' +sudo: false +install: + - cabal install +script: + - bash test.sh diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..65bba9e --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,24 @@ +# Changelog + +- 0.3.3.0 (2016-10-31) + * Add a `--version` flag. + * Add support for `pandoc-1.18` which includes a new `LineBlock` element. + +- 0.3.2.0 (2016-10-20) + * Keep running even if errors are encountered during reload. + +- 0.3.1.0 (2016-10-18) + * Fix compilation with `lts-6.22`. + +- 0.3.0.0 (2016-10-17) + * Add syntax highlighting support. + * Fixed slide clipping after reload. + +- 0.2.0.0 (2016-10-13) + * Add theming support. + * Fix links display. + * Add support for wrapping. + * Allow org mode as input format. + +- 0.1.0.0 (2016-10-02) + * Upload first version from hotel wifi in Kalaw. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..1f53f40 --- /dev/null +++ b/LICENSE @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..3cd2820 --- /dev/null +++ b/README.md @@ -0,0 +1,290 @@ +patat +===== + +[![Build Status](https://img.shields.io/travis/jaspervdj/patat.svg)](https://travis-ci.org/jaspervdj/patat) [![Hackage](https://img.shields.io/hackage/v/patat.svg)](https://hackage.haskell.org/package/patat) [![GitHub tag](https://img.shields.io/github/tag/jaspervdj/patat.svg)]() + +`patat` (**P**resentations **A**top **T**he **A**NSI **T**erminal) is a small +tool that allows you to show presentations using only an ANSI terminal. It does +not require `ncurses`. + +Features: + +- Leverages the great [Pandoc] library to support many input formats including + [Literate Haskell]. +- Supports [smart slide splitting](#input-format). +- There is a [live reload](#running) mode. +- [Theming](#theming) support. +- Optionally [re-wrapping](#configuration) text to terminal width with proper + indentation. +- Syntax highlighting for nearly one hundred languages generated from [Kate] + syntax files. +- Written in [Haskell]. + +![screenshot](extra/screenshot.png?raw=true) + +[Kate]: https://kate-editor.org/ +[Haskell]: http://haskell.org/ +[Pandoc]: http://pandoc.org/ + +Table of Contents +----------------- + +- [Installation](#installation) + - [Using stack](#using-stack) + - [Using cabal](#using-cabal) +- [Running](#running) +- [Input format](#input-format) +- [Configuration](#configuration) + - [Theming](#theming) + - [Syntax Highlighting](#syntax-highlighting) +- [Trivia](#trivia) + +Installation +------------ + +You can build from source using `stack install` or `cabal install`. `patat` is +also available from [Hackage]. + +[Hackage]: https://hackage.haskell.org/package/patat + +For people unfamiliar with the Haskell ecosystem, this means you can do either +of the following: + +### Using stack + +1. Install [stack] for your platform. +2. Clone this repository. +3. Run `stack setup` (if you're running stack for the first time) and + `stack install`. +4. Make sure `$HOME/.local/bin` is in your `$PATH`. + +[stack]: https://docs.haskellstack.org/en/stable/README/ + +### Using cabal + +1. Install [cabal] for your platform. +2. Run `cabal install patat`. +3. Make sure `$HOME/.cabal/bin` is in your `$PATH`. + +[cabal]: https://www.haskell.org/cabal/ + +Running +------- + + patat [--watch] presentation.md + +Controls: + +- **Next slide**: `space`, `enter`, `l`, `→` +- **Previous slide**: `backspace`, `h`, `←` +- **Go forward 10 slides**: `j`, `↓` +- **Go backward 10 slides**: `k`, `↑` +- **First slide**: `0` +- **Last slide**: `G` +- **Reload file**: `r` +- **Quit**: `q` + +The `r` key is very useful since it allows you to preview your slides while you +are writing them. You can also use this to fix artifacts when the terminal is +resized. + +If you provide the `--watch` flag, `patat` will watch the presentation file for +changes and reload automatically. This is very useful when you are writing the +presentation. + +Input format +------------ + +The input format can be anything that Pandoc supports. Plain markdown is +usually the most simple solution: + + --- + title: This is my presentation + author: Jane Doe + ... + + # This is a slide + + Slide contents. Yay. + + --- + + # Important title + + Things I like: + + - Markdown + - Haskell + - Pandoc + +Horizontal rulers (`---`) are used to split slides. + +However, if you prefer not use these since they are a bit intrusive in the +markdown, you can also start every slide with an `h1` header. In that case, the +file should not contain a single horizontal ruler. + +This means the following document is equivalent: + + --- + title: This is my presentation + author: Jane Doe + ... + + # This is a slide + + Slide contents. Yay. + + # Important title + + Things I like: + + - Markdown + - Haskell + - Pandoc + +Configuration +------------- + +`patat` is fairly configurable. The configuration is done using [YAML]. There +are two places where you can put your configuration: + +1. In the presentation file itself, using the [Pandoc metadata header]. +2. In `$HOME/.patat.yaml` + +[YAML]: http://yaml.org/ +[Pandoc metadata header]: http://pandoc.org/MANUAL.html#extension-yaml_metadata_block + +For example, we can turn on line wrapping by using the following file: + + --- + title: Presentation with wrapping + author: John Doe + patat: + wrap: true + ... + + This is a split + line which should + be re-wrapped. + +Or we can use a normal presentation and have the following `$HOME/.patat.yaml`: + + wrap: true + +### Theming + +Colors and other properties can also be changed using this configuration. For +example, we can have: + + --- + author: 'Jasper Van der Jeugt' + title: 'This is a test' + patat: + wrap: true + theme: + emph: [vividBlue, onVividBlack, bold] + imageTarget: [onDullWhite, vividRed] + ... + + # This is a presentation + + This is _emph_ text. + + ![Hello](foo.png) + +The properties that can be given a list of styles are: + +- `borders` +- `header` +- `codeBlock` +- `bulletList` +- `orderedList` +- `blockQuote` +- `definitionTerm` +- `definitionList` +- `tableHeader` +- `tableSeparator` +- `emph` +- `strong` +- `code` +- `linkText` +- `linkTarget` +- `strikeout` +- `quoted` +- `math` +- `imageText` +- `imageTarget` + +The accepted styles are: + +- `bold` +- `dullBlack` +- `dullBlue` +- `dullCyan` +- `dullGreen` +- `dullMagenta` +- `dullRed` +- `dullWhite` +- `dullYellow` +- `onDullBlack` +- `onDullBlue` +- `onDullCyan` +- `onDullGreen` +- `onDullMagenta` +- `onDullRed` +- `onDullWhite` +- `onDullYellow` +- `onVividBlack` +- `onVividBlue` +- `onVividCyan` +- `onVividGreen` +- `onVividMagenta` +- `onVividRed` +- `onVividWhite` +- `onVividYellow` +- `underline` +- `vividBlack` +- `vividBlue` +- `vividCyan` +- `vividGreen` +- `vividMagenta` +- `vividRed` +- `vividWhite` +- `vividYellow` + +### Syntax Highlighting + +As part of theming, syntax highlighting is also configurable. This can be +configured like this: + + --- + patat: + theme: + syntaxHighlighting: + decVal: [bold, onDullRed] + ... + + ... + +`decVal` refers to "decimal values". This is known as a "token type". For a +full list of token types, see [this list] -- the names are derived from there in +an obvious way. + +[this list]: https://hackage.haskell.org/package/highlighting-kate-0.6.3/docs/Text-Highlighting-Kate-Types.html#t:TokenType + +Trivia +------ + +_"Patat"_ is the Flemish word for a simple potato. Dutch people also use it to +refer to French Fries but I don't really do that -- in Belgium we just call +fries _"Frieten"_. + +The idea of `patat` is largely based upon [MDP] which is in turn based upon +[VTMC]. I wanted to write a clone using Pandoc because I ran into a markdown +parsing bug in MDP which I could not work around. A second reason to do a +Pandoc-based tool was that I would be able to use [Literate Haskell] as well. +Lastly, I also prefer not to install Node.js on my machine if I can avoid it. + +[MDP]: https://github.com/visit1985/mdp +[VTMC]: https://github.com/jclulow/vtmc +[Literate Haskell]: https://wiki.haskell.org/Literate_programming diff --git a/Setup.hs b/Setup.hs new file mode 100644 index 0000000..9a994af --- /dev/null +++ b/Setup.hs @@ -0,0 +1,2 @@ +import Distribution.Simple +main = defaultMain diff --git a/debian/changelog b/debian/changelog new file mode 100644 index 0000000..35e9936 --- /dev/null +++ b/debian/changelog @@ -0,0 +1,5 @@ +patat (0.3.2.0-1) unstable; urgency=medium + + * Initial release (Closes: #840738) + + -- Félix Sipma Tue, 25 Oct 2016 23:46:09 +0200 diff --git a/debian/compat b/debian/compat new file mode 100644 index 0000000..f599e28 --- /dev/null +++ b/debian/compat @@ -0,0 +1 @@ +10 diff --git a/debian/control b/debian/control new file mode 100644 index 0000000..c1523d1 --- /dev/null +++ b/debian/control @@ -0,0 +1,40 @@ +Source: patat +Maintainer: Félix Sipma +Priority: extra +Section: haskell +Build-Depends: debhelper (>= 10), + ghc, + libghc-aeson-dev (>= 0.11), + libghc-aeson-dev (<< 1.1), + libghc-ansi-terminal-dev (>= 0.6), + libghc-ansi-terminal-dev (<< 0.7), + libghc-ansi-wl-pprint-dev (>= 0.6), + libghc-ansi-wl-pprint-dev (<< 0.7), + libghc-highlighting-kate-dev (>= 0.6), + libghc-highlighting-kate-dev (<< 0.7), + libghc-mtl-dev (>= 2.2), + libghc-mtl-dev (<< 2.3), + libghc-optparse-applicative-dev (>= 0.12), + libghc-optparse-applicative-dev (<< 0.14), + libghc-pandoc-dev (>= 1.17), + libghc-pandoc-dev (<< 1.18), + libghc-terminal-size-dev (>= 0.3), + libghc-terminal-size-dev (<< 0.4), + libghc-text-dev (>= 1.2), + libghc-text-dev (<< 1.3), + libghc-yaml-dev (>= 0.7), + libghc-yaml-dev (<< 0.9), + pandoc +Standards-Version: 3.9.8 +Homepage: http://github.com/jaspervdj/patat +Vcs-Git: https://git.gueux.org/patat.git +Vcs-Browser: https://git.gueux.org/?p=patat.git;a=summary + +Package: patat +Architecture: any +Section: misc +Depends: ${shlibs:Depends}, ${haskell:Depends}, ${misc:Depends} +Description: Terminal-based presentations using Pandoc + patat (*P*resentations *A*top *T*he *A*NSI *T*erminal) is a small tool that + allows you to show presentations using only an ANSI terminal. It does not + require `ncurses`. diff --git a/debian/copyright b/debian/copyright new file mode 100644 index 0000000..f9fa260 --- /dev/null +++ b/debian/copyright @@ -0,0 +1,29 @@ +Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ +Upstream-Name: patat +Upstream-Contact: Jasper Van der Jeugt +Source: https://hackage.haskell.org/package/patat + +Files: * +Copyright: 2016 Jasper Van der Jeugt +License: GPL-2 + +Files: debian/* +Copyright: 2016 Félix Sipma +License: GPL-2 + +License: GPL-2 + This package is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License version 2 as + published by the Free Software Foundation. + . + This package is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + . + You should have received a copy of the GNU General Public License + along with this package; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + . + On Debian systems, the complete text of the GNU General Public License version + 2 can be found in `/usr/share/common-licenses/GPL-2'. diff --git a/debian/docs b/debian/docs new file mode 100644 index 0000000..b43bf86 --- /dev/null +++ b/debian/docs @@ -0,0 +1 @@ +README.md diff --git a/debian/lintian-overrides b/debian/lintian-overrides new file mode 100644 index 0000000..172c637 --- /dev/null +++ b/debian/lintian-overrides @@ -0,0 +1,2 @@ +# standard override for Haskell binary packages +binary-or-shlib-defines-rpath diff --git a/debian/patat.examples b/debian/patat.examples new file mode 100644 index 0000000..56ab4af --- /dev/null +++ b/debian/patat.examples @@ -0,0 +1 @@ +extra/screenshot.png diff --git a/debian/patat.install b/debian/patat.install new file mode 100644 index 0000000..3d405a1 --- /dev/null +++ b/debian/patat.install @@ -0,0 +1 @@ +dist/build/patat/patat usr/bin diff --git a/debian/patat.manpages b/debian/patat.manpages new file mode 100644 index 0000000..fdcdd4e --- /dev/null +++ b/debian/patat.manpages @@ -0,0 +1 @@ +debian/patat.1 diff --git a/debian/patat.md b/debian/patat.md new file mode 100644 index 0000000..34717ee --- /dev/null +++ b/debian/patat.md @@ -0,0 +1,48 @@ +% patat(1) +% [Jasper Van der Jeugt](mailto:m@jaspervdj.be) +% October 2016 + +Name +==== + +patat - Terminal-based presentations using Pandoc + +Synopsis +======== + +`patat` [*OPTION* ...] [*FILENAME*] + +Description +=========== + +`patat` (**P**resentations **A**top **T**he **A**NSI **T**erminal) is a small +tool that allows you to show presentations using only an ANSI terminal. It +does not require `ncurses`. + +Options +======= + +`-h`,`--help` + +: Show this help text + +`FILENAME` + +: Input file + +`-f`,`--force` + +: Force ANSI terminal + +`-d`,`--dump` + +: Just dump all slides and exit + +`-w`,`--watch` + +: Watch file for changes + +`--version` + +: Display version info and exit + diff --git a/debian/patches/0001-Debianize-README.md.patch b/debian/patches/0001-Debianize-README.md.patch new file mode 100644 index 0000000..db4b94d --- /dev/null +++ b/debian/patches/0001-Debianize-README.md.patch @@ -0,0 +1,78 @@ +From: =?utf-8?q?F=C3=A9lix_Sipma?= +Date: Thu, 20 Oct 2016 15:09:42 +0200 +Subject: Debianize README.md + +- remove installation instructions and CI links +- modify link to screenshot file +--- + README.md | 36 +----------------------------------- + 1 file changed, 1 insertion(+), 35 deletions(-) + +diff --git a/README.md b/README.md +index 3cd2820..3dfcdf6 100644 +--- a/README.md ++++ b/README.md +@@ -1,8 +1,6 @@ + patat + ===== + +-[![Build Status](https://img.shields.io/travis/jaspervdj/patat.svg)](https://travis-ci.org/jaspervdj/patat) [![Hackage](https://img.shields.io/hackage/v/patat.svg)](https://hackage.haskell.org/package/patat) [![GitHub tag](https://img.shields.io/github/tag/jaspervdj/patat.svg)]() +- + `patat` (**P**resentations **A**top **T**he **A**NSI **T**erminal) is a small + tool that allows you to show presentations using only an ANSI terminal. It does + not require `ncurses`. +@@ -20,7 +18,7 @@ Features: + syntax files. + - Written in [Haskell]. + +-![screenshot](extra/screenshot.png?raw=true) ++![screenshot](examples/screenshot.png?raw=true) + + [Kate]: https://kate-editor.org/ + [Haskell]: http://haskell.org/ +@@ -29,9 +27,6 @@ Features: + Table of Contents + ----------------- + +-- [Installation](#installation) +- - [Using stack](#using-stack) +- - [Using cabal](#using-cabal) + - [Running](#running) + - [Input format](#input-format) + - [Configuration](#configuration) +@@ -39,35 +34,6 @@ Table of Contents + - [Syntax Highlighting](#syntax-highlighting) + - [Trivia](#trivia) + +-Installation +------------- +- +-You can build from source using `stack install` or `cabal install`. `patat` is +-also available from [Hackage]. +- +-[Hackage]: https://hackage.haskell.org/package/patat +- +-For people unfamiliar with the Haskell ecosystem, this means you can do either +-of the following: +- +-### Using stack +- +-1. Install [stack] for your platform. +-2. Clone this repository. +-3. Run `stack setup` (if you're running stack for the first time) and +- `stack install`. +-4. Make sure `$HOME/.local/bin` is in your `$PATH`. +- +-[stack]: https://docs.haskellstack.org/en/stable/README/ +- +-### Using cabal +- +-1. Install [cabal] for your platform. +-2. Run `cabal install patat`. +-3. Make sure `$HOME/.cabal/bin` is in your `$PATH`. +- +-[cabal]: https://www.haskell.org/cabal/ +- + Running + ------- + diff --git a/debian/patches/0002-add-version.patch b/debian/patches/0002-add-version.patch new file mode 100644 index 0000000..32564a2 --- /dev/null +++ b/debian/patches/0002-add-version.patch @@ -0,0 +1,95 @@ +From: =?utf-8?q?F=C3=A9lix_Sipma?= +Date: Tue, 25 Oct 2016 14:47:45 +0200 +Subject: add --version + +--- + src/Main.hs | 32 ++++++++++++++++++++++++++------ + 1 file changed, 26 insertions(+), 6 deletions(-) + +diff --git a/src/Main.hs b/src/Main.hs +index 6527cbd..fa434da 100644 +--- a/src/Main.hs ++++ b/src/Main.hs +@@ -10,7 +10,7 @@ import Control.Applicative ((<$>), (<*>)) + import Control.Concurrent (forkIO, threadDelay) + import qualified Control.Concurrent.Chan as Chan + import Control.Monad (forever, unless, when) +-import Data.Monoid ((<>)) ++import Data.Monoid (mempty, (<>)) + import Data.Time (UTCTime) + import Data.Version (showVersion) + import qualified Options.Applicative as OA +@@ -19,7 +19,7 @@ import qualified Paths_patat + import qualified System.Console.ANSI as Ansi + import System.Directory (doesFileExist, + getModificationTime) +-import System.Exit (exitFailure) ++import System.Exit (exitFailure, exitSuccess) + import qualified System.IO as IO + import qualified Text.PrettyPrint.ANSI.Leijen as PP + import Prelude +@@ -27,17 +27,18 @@ import Prelude + + -------------------------------------------------------------------------------- + data Options = Options +- { oFilePath :: !FilePath ++ { oFilePath :: !(Maybe FilePath) + , oForce :: !Bool + , oDump :: !Bool + , oWatch :: !Bool ++ , oVersion :: !Bool + } deriving (Show) + + + -------------------------------------------------------------------------------- + parseOptions :: OA.Parser Options + parseOptions = Options +- <$> (OA.strArgument $ ++ <$> (OA.optional $ OA.strArgument $ + OA.metavar "FILENAME" <> + OA.help "Input file") + <*> (OA.switch $ +@@ -54,6 +55,10 @@ parseOptions = Options + OA.long "watch" <> + OA.short 'w' <> + OA.help "Watch file for changes") ++ <*> (OA.switch $ ++ OA.long "version" <> ++ OA.help "Display version info and exit" <> ++ OA.hidden) + + + -------------------------------------------------------------------------------- +@@ -79,6 +84,11 @@ parserInfo = OA.info (OA.helper <*> parseOptions) $ + + + -------------------------------------------------------------------------------- ++parserPrefs :: OA.ParserPrefs ++parserPrefs = OA.prefs OA.showHelpOnError ++ ++ ++-------------------------------------------------------------------------------- + errorAndExit :: [String] -> IO a + errorAndExit msg = do + mapM_ (IO.hPutStrLn IO.stderr) msg +@@ -98,8 +108,18 @@ assertAnsiFeatures = do + -------------------------------------------------------------------------------- + main :: IO () + main = do +- options <- OA.customExecParser (OA.prefs OA.showHelpOnError) parserInfo +- errOrPres <- readPresentation (oFilePath options) ++ options <- OA.customExecParser parserPrefs parserInfo ++ ++ when (oVersion options) $ do ++ putStrLn (showVersion Paths_patat.version) ++ exitSuccess ++ ++ filePath <- case oFilePath options of ++ Just fp -> return fp ++ Nothing -> OA.handleParseResult $ OA.Failure $ ++ OA.parserFailure parserPrefs parserInfo OA.ShowHelpText mempty ++ ++ errOrPres <- readPresentation filePath + pres <- either (errorAndExit . return) return errOrPres + + unless (oForce options) assertAnsiFeatures diff --git a/debian/patches/series b/debian/patches/series new file mode 100644 index 0000000..3ade4dc --- /dev/null +++ b/debian/patches/series @@ -0,0 +1,2 @@ +0001-Debianize-README.md.patch +0002-add-version.patch diff --git a/debian/rules b/debian/rules new file mode 100755 index 0000000..a640af6 --- /dev/null +++ b/debian/rules @@ -0,0 +1,26 @@ +#!/usr/bin/make -f + +# these rules originally written by Joey Hess for hothasktags package + +%: + dh $@ + +override_dh_auto_configure: + ghc --make Setup + ./Setup configure + +override_dh_auto_build: + pandoc -sS debian/patat.md -o debian/patat.1 + ./Setup build + +override_dh_auto_clean: + if [ -x Setup ]; then ./Setup clean; fi + rm -f Setup Setup.o Setup.hi debian/patat.1 + +override_dh_auto_test: + PATH="$(PATH):dist/build/patat/" bash test.sh + +override_dh_strip: + # GHC cannot produce debugging symbols so the -dbgsym package + # ends up empty, so disable generating it + dh_strip --no-automatic-dbgsym diff --git a/debian/source/format b/debian/source/format new file mode 100644 index 0000000..163aaf8 --- /dev/null +++ b/debian/source/format @@ -0,0 +1 @@ +3.0 (quilt) diff --git a/debian/watch b/debian/watch new file mode 100644 index 0000000..95dbdf1 --- /dev/null +++ b/debian/watch @@ -0,0 +1,4 @@ +version=3 + +opts=filenamemangle=s/.+\/v?(\d\S*)\.tar\.gz/patat-$1\.tar\.gz/ \ + https://github.com/jaspervdj/patat/tags .*/v?(\d\S*)\.tar\.gz diff --git a/extra/screenshot.png b/extra/screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..afc45b07cbdfff99fac80087f69a1ab0b3b3cdeb GIT binary patch literal 34652 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�D*`NgNn7x)Imv?@g_<@Y7b*`K@g%kzd8}grPh> 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≶0$-0THr#*m^c`+!f&1aPrTt9y+kbz?hrjCd#}z8r z$B} z-Rg{i1`;`Et+%;f2dicT6b#AS1-8WlzXgYdNmsL2TDq|k5jfX2?;}G%_B7g}bJN$- zOQcKh)f0?>aqD!MBwq3bWV>u^!LdB$lLGRJ2b0x$#F%0{x(EvcYf45JiN$Bu)QfHn z;;zjmJIgBXx~Yy)KEx;GG+6}q*Y5B|z{J+R_32sYa)(hm%hu^8KgUw$^=WcM7x!QIo}|-BHRZ zK-90>45@qD5!lOu4LM!#jJjow$x@Hp#+jqU)ixxb#Vr3^Y&Gm z=cI)R%{TG(qT80Ne5|lgWX1KA3WJ@QB$1T=a@gi9>pB6ZJJ>PGE6mHDFPb%B^lK~ai4V#qMeERZmInsWyxW0}mF zZ!iXd>XFLrdFcZ+g@LDE;Zr33c*@*d+LbL6Fz|`ap}a zgVrWWi=#&OlC!G|ugu&Z?O0ZBQXwq@EzPgAXbO>z&oav1A;unso=uoIhIPpn<3ELt zl7dArFM-|;$(imWBNg#k+U9GlV`5=5pH=%HS*k2MER+YX8R%)tAFTE#{Zq-Ha1&JA zC+Y_&rcjbtq&$IFjiNHow~~ksDx^ug?(PM^TmH~J?9rL@$9c-IF%hd(ach?O@~O?Hw&^EN)A8xe zedA2?qQr|P9J;dar>zLCi#yh;PW!9qIg7FRyYVG-w(-vD{cr92- sI-iPj%#uT$ ztElPea_~>PywLY$q5t$*tJ19pS(<8~TxyPlj$h(n9(nJ(>hQEPCy1sK(X6PDBLf+u zT)Cal_Z~td-|a#PTv=tP;`Cv#)K$i+V3<%>b6IA_VJm=}0t$}8{u%i$#j5%?H|UBn??o@lOZ(`lj5DnTP(%{eL#&64&@g2HCQQCJ^Uc@k?#exb&^DLGn5Q+W9q z;AHrdb$`kb(TUCrw!1VO7jVW91d7v5MwgUNmwWawTH4B?PB~lU%(Y!^5T`a;?$YR{ zhNAqC#v&@Qu_rDH`%mMy&XboZO|ac~0X%SN-Y~VZ$#_1YtBQgPdpHPM8OKcdLq^<) z)t+`sX|N(5#X7&a?$xDR>t0qYVd;{^d>lo^JV^})E&{M0181OW=+n(JnqL~rJxd_; zjno?IKP--YTS%0)jHL2Wc2NOXCD+PeNMI) zBP}Tf)QK@UH(p@drw2`g^YK2xtCHHU-oHBlUX5mBbreDjPa3@r>{A;qtaZH2Ha2%j zv9Zz{*V)s%#3(G8lN+|yov&@guJo*I=mU%GYsQd3Q1+MeBZ+URwR0w2E!GbeTqbDF z*J%?UtDo1Z)TbuhN$LF~E8B=C7}<4yoSFq zs7$YSCEedY%|pd8M!^xAp50A!pFt1NHMB5&&n*i9tj7SrJ-U=>;M6 zufz=-OkCahQM!?AN%EMPMGo}SPnAfqd3LWjL(Dcc;WX4WW4hRrfWt$Sh`pqnnYraA zb1iu30qv2_s*5S0aOfq6yOf%_;Zb!|2QwJhr(FqQ0i~u2xGGX6SKJ7R_R2C~JRJqF zB-O`5vA+IZ{In#5)5oi>rtEe2exEQ+@f2w_&fUH`>O{&RRO@nsg?vGV3t+zjCLfv< zy0#UYn*6hHUDd8IkLsiXl@Xpbyoi#@kiY5`CND`0<>e9HV=c}rPbo20(`Py~YUkCW z_fA4qk7n>HH#eRIeVV)Z##~+5Wr#ta5HGk4h%TMDkr4lLQ4f7HbS|}w;`z;d3 zu}i6;HO)F58PtOcleJ%RAd~pppTUEC7O$_DSs*tjy&H>s8Pw!T}|&5EtWQZg=NjBa;DisWxN9Dl)NVMCTk8GYEM2pQCF0| zuUX*5c3jI@aQ*5wwHvKgG*|=N8P%(s5Ev`+Zhn4l99D*w?$U%Gs%)J}EA~IwOSGduE3rN;6hz=C##zV_GN)JVgI z9kQL89jKZFkoFYuwVrQg`3TsT3eudRhs?3uX<_nzGy3S)gMxK(@ijfW`do4Ty0QuP zLyicdw3V!Rx%Ekt>1@X=@yPXQW$;XO&Sn#H+ue3uVY_AMLcWk4O=_+KdL?KexBRJm|ORjgBw5l?#k*B#(J zi_tmEB#h)1i7ZuGj-_SlWuf05j!B6Ol^;z#nc$+llX~)HrpTzrw6(*{&qmW3OEnW8 zmoH>Cbje(sf}RxMn>z4O3E-eVhjl|>Pc@ zI3fxmk>My-s}sWAe^nek$i{aNKY4u6Zul%APQwI5-)#SGOA%ptm}T$2kIMH5edEl} zb5fP=62-x~WE>>N^)mA^-UohF@N8~x+G8LG6x%&KfiZcy1aJAcVrVQjP*Bw=yL{7Gmo`y*vj zHqLQgG9ol8ESR4FN-EM0wxlFgqy21#Gq)rjUVGeJYPljhn#dpNHeyl@G#igYj(*-@W0R1jhqO%Q&-8Knqk-V_@bVZ~-D+KfFaerHXdKBF1BX`yN${~ zm&zWY3EfU6j}UOOFTkg(Jgdc)S4 zhSg7_G9O^vL(M7q9`Zy=P3aZ4_s+xl1v$hu@}Ziqhrz>iRtnlM?W`%fPKr6c%BI+T zMo7@UtPT!|bwx@UfSdE|jFr>JW4UQL8&&etT~A?v2}yPfCb(~`b5H$fu{Vb=J(1HI z`^ZV!q5X4ayd;8=?_-YnsVXiV)m0bO4#qHB4wOx)FAS@(z{U1Qbqmb+qi#!WI)0K88s#{Ngk>ZKTgk^oW6^gSyEqYkaw*F4y0AB;l=otTdy!I#ifz=dT#=-iQ z8xUQxpq~H#0h*7z9{>OV literal 0 HcmV?d00001 diff --git a/patat.cabal b/patat.cabal new file mode 100644 index 0000000..35bcadc --- /dev/null +++ b/patat.cabal @@ -0,0 +1,57 @@ +Name: patat +Version: 0.3.3.0 +Synopsis: Terminal-based presentations using Pandoc +Description: Terminal-based presentations using Pandoc +License: GPL-2 +License-file: LICENSE +Author: Jasper Van der Jeugt +Maintainer: Jasper Van der Jeugt +Homepage: http://github.com/jaspervdj/patat +Copyright: 2016 Jasper Van der Jeugt +Category: Text +Build-type: Simple +Extra-source-files: CHANGELOG.md +Cabal-version: >=1.10 + +Source-repository head + Type: git + Location: git://github.com/jaspervdj/patat.git + +Executable patat + Main-is: Main.hs + Ghc-options: -Wall -threaded -rtsopts "-with-rtsopts=-N" + Hs-source-dirs: src + Default-language: Haskell2010 + + Build-depends: + aeson >= 0.9 && < 1.1, + ansi-terminal >= 0.6 && < 0.7, + ansi-wl-pprint >= 0.6 && < 0.7, + base >= 4.6 && < 4.10, + bytestring >= 0.10 && < 0.11, + containers >= 0.5 && < 0.6, + directory >= 1.2 && < 1.3, + filepath >= 1.4 && < 1.5, + highlighting-kate >= 0.6 && < 0.7, + mtl >= 2.2 && < 2.3, + optparse-applicative >= 0.12 && < 0.14, + pandoc >= 1.16 && < 1.19, + terminal-size >= 0.3 && < 0.4, + text >= 1.2 && < 1.3, + time >= 1.4 && < 1.7, + yaml >= 0.7 && < 0.9 + + Other-modules: + Data.Aeson.Extended + Data.Aeson.TH.Extended + Data.Data.Extended + Patat.Presentation + Patat.Presentation.Display + Patat.Presentation.Display.CodeBlock + Patat.Presentation.Display.Table + Patat.Presentation.Interactive + Patat.Presentation.Internal + Patat.Presentation.Read + Patat.PrettyPrint + Patat.Theme + Text.Pandoc.Extended diff --git a/src/Data/Aeson/Extended.hs b/src/Data/Aeson/Extended.hs new file mode 100644 index 0000000..9b95cec --- /dev/null +++ b/src/Data/Aeson/Extended.hs @@ -0,0 +1,22 @@ +{-# LANGUAGE GeneralizedNewtypeDeriving #-} +module Data.Aeson.Extended + ( module Data.Aeson + + , FlexibleNum (..) + ) where + +import Control.Applicative ((<$>)) +import Data.Aeson +import qualified Data.Text as T +import Text.Read (readMaybe) +import Prelude + +-- | This can be parsed from a JSON string in addition to a JSON number. +newtype FlexibleNum a = FlexibleNum {unFlexibleNum :: a} + deriving (Show, ToJSON) + +instance (FromJSON a, Read a) => FromJSON (FlexibleNum a) where + parseJSON (String str) = case readMaybe (T.unpack str) of + Nothing -> fail $ "Could not parse " ++ T.unpack str ++ " as a number" + Just x -> return (FlexibleNum x) + parseJSON val = FlexibleNum <$> parseJSON val diff --git a/src/Data/Aeson/TH/Extended.hs b/src/Data/Aeson/TH/Extended.hs new file mode 100644 index 0000000..0fa5487 --- /dev/null +++ b/src/Data/Aeson/TH/Extended.hs @@ -0,0 +1,21 @@ +-------------------------------------------------------------------------------- +module Data.Aeson.TH.Extended + ( module Data.Aeson.TH + , dropPrefixOptions + ) where + + +-------------------------------------------------------------------------------- +import Data.Aeson.TH +import Data.Char (isUpper, toLower) + + +-------------------------------------------------------------------------------- +dropPrefixOptions :: Options +dropPrefixOptions = defaultOptions + { fieldLabelModifier = dropPrefix + } + where + dropPrefix str = case break isUpper str of + (_, (y : ys)) -> toLower y : ys + _ -> str diff --git a/src/Data/Data/Extended.hs b/src/Data/Data/Extended.hs new file mode 100644 index 0000000..636591e --- /dev/null +++ b/src/Data/Data/Extended.hs @@ -0,0 +1,23 @@ +module Data.Data.Extended + ( module Data.Data + + , grecQ + , grecT + ) where + +import Data.Data + +-- | Recursively find all values of a certain type. +grecQ :: (Data a, Data b) => a -> [b] +grecQ = concat . gmapQ (\x -> maybe id (:) (cast x) $ grecQ x) + +-- | Recursively apply an update to a certain type. +grecT :: (Data a, Data b) => (a -> a) -> b -> b +grecT f x = gmapT (grecT f) (castMap f x) + +castMap :: (Typeable a, Typeable b) => (a -> a) -> b -> b +castMap f x = case cast x of + Nothing -> x + Just y -> case cast (f y) of + Nothing -> x + Just z -> z diff --git a/src/Main.hs b/src/Main.hs new file mode 100644 index 0000000..fa434da --- /dev/null +++ b/src/Main.hs @@ -0,0 +1,170 @@ +-------------------------------------------------------------------------------- +{-# LANGUAGE GeneralizedNewtypeDeriving #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE RecordWildCards #-} +module Main where + + +-------------------------------------------------------------------------------- +import Control.Applicative ((<$>), (<*>)) +import Control.Concurrent (forkIO, threadDelay) +import qualified Control.Concurrent.Chan as Chan +import Control.Monad (forever, unless, when) +import Data.Monoid (mempty, (<>)) +import Data.Time (UTCTime) +import Data.Version (showVersion) +import qualified Options.Applicative as OA +import Patat.Presentation +import qualified Paths_patat +import qualified System.Console.ANSI as Ansi +import System.Directory (doesFileExist, + getModificationTime) +import System.Exit (exitFailure, exitSuccess) +import qualified System.IO as IO +import qualified Text.PrettyPrint.ANSI.Leijen as PP +import Prelude + + +-------------------------------------------------------------------------------- +data Options = Options + { oFilePath :: !(Maybe FilePath) + , oForce :: !Bool + , oDump :: !Bool + , oWatch :: !Bool + , oVersion :: !Bool + } deriving (Show) + + +-------------------------------------------------------------------------------- +parseOptions :: OA.Parser Options +parseOptions = Options + <$> (OA.optional $ OA.strArgument $ + OA.metavar "FILENAME" <> + OA.help "Input file") + <*> (OA.switch $ + OA.long "force" <> + OA.short 'f' <> + OA.help "Force ANSI terminal" <> + OA.hidden) + <*> (OA.switch $ + OA.long "dump" <> + OA.short 'd' <> + OA.help "Just dump all slides and exit" <> + OA.hidden) + <*> (OA.switch $ + OA.long "watch" <> + OA.short 'w' <> + OA.help "Watch file for changes") + <*> (OA.switch $ + OA.long "version" <> + OA.help "Display version info and exit" <> + OA.hidden) + + +-------------------------------------------------------------------------------- +parserInfo :: OA.ParserInfo Options +parserInfo = OA.info (OA.helper <*> parseOptions) $ + OA.fullDesc <> + OA.header ("patat v" <> showVersion Paths_patat.version) <> + OA.progDescDoc (Just desc) + where + desc = PP.vcat + [ "Terminal-based presentations using Pandoc" + , "" + , "Controls:" + , "- Next slide: space, enter, l, right" + , "- Previous slide: backspace, h, left" + , "- Go forward 10 slides: j, down" + , "- Go backward 10 slides: k, up" + , "- First slide: 0" + , "- Last slide: G" + , "- Reload file: r" + , "- Quit: q" + ] + + +-------------------------------------------------------------------------------- +parserPrefs :: OA.ParserPrefs +parserPrefs = OA.prefs OA.showHelpOnError + + +-------------------------------------------------------------------------------- +errorAndExit :: [String] -> IO a +errorAndExit msg = do + mapM_ (IO.hPutStrLn IO.stderr) msg + exitFailure + + +-------------------------------------------------------------------------------- +assertAnsiFeatures :: IO () +assertAnsiFeatures = do + supports <- Ansi.hSupportsANSI IO.stdout + unless supports $ errorAndExit + [ "It looks like your terminal does not support ANSI codes." + , "If you still want to run the presentation, use `--force`." + ] + + +-------------------------------------------------------------------------------- +main :: IO () +main = do + options <- OA.customExecParser parserPrefs parserInfo + + when (oVersion options) $ do + putStrLn (showVersion Paths_patat.version) + exitSuccess + + filePath <- case oFilePath options of + Just fp -> return fp + Nothing -> OA.handleParseResult $ OA.Failure $ + OA.parserFailure parserPrefs parserInfo OA.ShowHelpText mempty + + errOrPres <- readPresentation filePath + pres <- either (errorAndExit . return) return errOrPres + + unless (oForce options) assertAnsiFeatures + + if oDump options + then dumpPresentation pres + else interactiveLoop options pres + where + interactiveLoop :: Options -> Presentation -> IO () + interactiveLoop options pres0 = do + IO.hSetBuffering IO.stdin IO.NoBuffering + commandChan <- Chan.newChan + + _ <- forkIO $ forever $ + readPresentationCommand >>= Chan.writeChan commandChan + + mtime0 <- getModificationTime (pFilePath pres0) + when (oWatch options) $ do + _ <- forkIO $ watcher commandChan (pFilePath pres0) mtime0 + return () + + let loop :: Presentation -> Maybe String -> IO () + loop pres mbError = do + case mbError of + Nothing -> displayPresentation pres + Just err -> displayPresentationError pres err + + c <- Chan.readChan commandChan + update <- updatePresentation c pres + case update of + ExitedPresentation -> return () + UpdatedPresentation pres' -> loop pres' Nothing + ErroredPresentation err -> loop pres (Just err) + + loop pres0 Nothing + + +-------------------------------------------------------------------------------- +watcher :: Chan.Chan PresentationCommand -> FilePath -> UTCTime -> IO a +watcher chan filePath mtime0 = do + -- The extra exists check helps because some editors temporarily make the + -- file dissapear while writing. + exists <- doesFileExist filePath + mtime1 <- if exists then getModificationTime filePath else return mtime0 + + when (mtime1 > mtime0) $ Chan.writeChan chan Reload + threadDelay (200 * 1000) + watcher chan filePath mtime1 diff --git a/src/Patat/Presentation.hs b/src/Patat/Presentation.hs new file mode 100644 index 0000000..8da5a30 --- /dev/null +++ b/src/Patat/Presentation.hs @@ -0,0 +1,20 @@ +module Patat.Presentation + ( PresentationSettings (..) + , defaultPresentationSettings + + , Presentation (..) + , readPresentation + , displayPresentation + , displayPresentationError + , dumpPresentation + + , PresentationCommand (..) + , readPresentationCommand + , UpdatedPresentation (..) + , updatePresentation + ) where + +import Patat.Presentation.Display +import Patat.Presentation.Interactive +import Patat.Presentation.Internal +import Patat.Presentation.Read diff --git a/src/Patat/Presentation/Display.hs b/src/Patat/Presentation/Display.hs new file mode 100644 index 0000000..99762e3 --- /dev/null +++ b/src/Patat/Presentation/Display.hs @@ -0,0 +1,311 @@ +-------------------------------------------------------------------------------- +{-# LANGUAGE CPP #-} +{-# LANGUAGE GeneralizedNewtypeDeriving #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE RecordWildCards #-} +module Patat.Presentation.Display + ( displayPresentation + , displayPresentationError + , dumpPresentation + ) where + + +-------------------------------------------------------------------------------- +import Control.Applicative ((<$>)) +import Control.Monad (mplus, unless) +import qualified Data.Aeson.Extended as A +import Data.Data.Extended (grecQ) +import Data.List (intersperse) +import Data.Maybe (fromMaybe) +import Data.Monoid (mconcat, mempty, (<>)) +import qualified Data.Text as T +import Patat.Presentation.Display.CodeBlock +import Patat.Presentation.Display.Table +import Patat.Presentation.Internal +import Patat.PrettyPrint ((<$$>), (<+>)) +import qualified Patat.PrettyPrint as PP +import Patat.Theme (Theme (..)) +import qualified Patat.Theme as Theme +import qualified System.Console.ANSI as Ansi +import qualified System.Console.Terminal.Size as Terminal +import qualified Text.Pandoc.Extended as Pandoc +import Prelude + + +-------------------------------------------------------------------------------- +-- | Display something within the presentation borders that draw the title and +-- the active slide number and so on. +displayWithBorders :: Presentation -> (Theme -> PP.Doc) -> IO () +displayWithBorders Presentation {..} f = do + Ansi.clearScreen + Ansi.setCursorPosition 0 0 + + -- Get terminal width/title + mbWindow <- Terminal.size + let columns = fromMaybe 72 $ + (A.unFlexibleNum <$> psColumns pSettings) `mplus` + (Terminal.width <$> mbWindow) + rows = fromMaybe 24 $ + (A.unFlexibleNum <$> psRows pSettings) `mplus` + (Terminal.height <$> mbWindow) + + let settings = pSettings {psColumns = Just $ A.FlexibleNum columns} + theme = fromMaybe Theme.defaultTheme (psTheme settings) + title = PP.toString (prettyInlines theme pTitle) + titleWidth = length title + titleOffset = (columns - titleWidth) `div` 2 + borders = themed (themeBorders theme) + + unless (null title) $ do + Ansi.setCursorColumn titleOffset + PP.putDoc $ borders $ PP.string title + putStrLn "" + putStrLn "" + + PP.putDoc $ withWrapSettings settings $ f theme + putStrLn "" + + let active = show (pActiveSlide + 1) ++ " / " ++ show (length pSlides) + activeWidth = length active + + Ansi.setCursorPosition (rows - 2) 0 + PP.putDoc $ " " <> borders (prettyInlines theme pAuthor) + Ansi.setCursorColumn (columns - activeWidth - 1) + PP.putDoc $ borders $ PP.string active + putStrLn "" + + +-------------------------------------------------------------------------------- +displayPresentation :: Presentation -> IO () +displayPresentation pres@Presentation {..} = displayWithBorders pres $ \theme -> + let slide = case drop pActiveSlide pSlides of + [] -> mempty + (s : _) -> s in + + prettySlide theme slide + + +-------------------------------------------------------------------------------- +-- | Displays an error in the place of the presentation. This is useful if we +-- want to display an error but keep the presentation running. +displayPresentationError :: Presentation -> String -> IO () +displayPresentationError pres err = displayWithBorders pres $ \Theme {..} -> + themed themeStrong "Error occurred in the presentation:" <$$> + "" <$$> + (PP.string err) + + +-------------------------------------------------------------------------------- +dumpPresentation :: Presentation -> IO () +dumpPresentation pres = + let theme = fromMaybe Theme.defaultTheme (psTheme $ pSettings pres) in + PP.putDoc $ withWrapSettings (pSettings pres) $ + PP.vcat $ intersperse "----------" $ + map (prettySlide theme) $ pSlides pres + + +-------------------------------------------------------------------------------- +withWrapSettings :: PresentationSettings -> PP.Doc -> PP.Doc +withWrapSettings ps = case (psWrap ps, psColumns ps) of + (Just True, Just (A.FlexibleNum col)) -> PP.wrapAt (Just col) + _ -> id + + +-------------------------------------------------------------------------------- +prettySlide :: Theme -> Slide -> PP.Doc +prettySlide theme slide@(Slide blocks) = + prettyBlocks theme blocks <> + case prettyReferences theme slide of + [] -> mempty + refs -> PP.hardline <> PP.vcat refs + + +-------------------------------------------------------------------------------- +prettyBlock :: Theme -> Pandoc.Block -> PP.Doc + +prettyBlock theme (Pandoc.Plain inlines) = prettyInlines theme inlines + +prettyBlock theme (Pandoc.Para inlines) = + prettyInlines theme inlines <> PP.hardline + +prettyBlock theme@Theme {..} (Pandoc.Header i _ inlines) = + themed themeHeader (PP.string (replicate i '#') <+> prettyInlines theme inlines) <> + PP.hardline + +prettyBlock theme (Pandoc.CodeBlock (_, classes, _) txt) = + prettyCodeBlock theme classes txt + +prettyBlock theme (Pandoc.BulletList bss) = PP.vcat + [ PP.indent + (PP.NotTrimmable $ themed (themeBulletList theme) prefix) + (PP.Trimmable " ") + (prettyBlocks theme' bs) + | bs <- bss + ] <> PP.hardline + where + prefix = " " <> PP.string [marker] <> " " + marker = case T.unpack <$> themeBulletListMarkers theme of + Just (x : _) -> x + _ -> '-' + + -- Cycle the markers. + theme' = theme + { themeBulletListMarkers = + (\ls -> T.drop 1 ls <> T.take 1 ls) <$> themeBulletListMarkers theme + } + +prettyBlock theme@Theme {..} (Pandoc.OrderedList _ bss) = PP.vcat + [ PP.indent + (PP.NotTrimmable $ themed themeOrderedList $ PP.string prefix) + (PP.Trimmable " ") + (prettyBlocks theme bs) + | (prefix, bs) <- zip padded bss + ] <> PP.hardline + where + padded = [n ++ replicate (4 - length n) ' ' | n <- numbers] + numbers = + [ show i ++ "." + | i <- [1 .. length bss] + ] + +prettyBlock _theme (Pandoc.RawBlock _ t) = PP.string t <> PP.hardline + +prettyBlock _theme Pandoc.HorizontalRule = "---" + +prettyBlock theme@Theme {..} (Pandoc.BlockQuote bs) = + let quote = PP.NotTrimmable (themed themeBlockQuote "> ") in + PP.indent quote quote (prettyBlocks theme bs) + +prettyBlock theme@Theme {..} (Pandoc.DefinitionList terms) = + PP.vcat $ map prettyDefinition terms + where + prettyDefinition (term, definitions) = + themed themeDefinitionTerm (prettyInlines theme term) <$$> + PP.hardline <> PP.vcat + [ PP.indent + (PP.NotTrimmable (themed themeDefinitionList ": ")) + (PP.Trimmable " ") $ + prettyBlocks theme (Pandoc.plainToPara definition) + | definition <- definitions + ] + +prettyBlock theme (Pandoc.Table caption aligns _ headers rows) = + PP.wrapAt Nothing $ + prettyTable theme Table + { tCaption = prettyInlines theme caption + , tAligns = map align aligns + , tHeaders = map (prettyBlocks theme) headers + , tRows = map (map (prettyBlocks theme)) rows + } + where + align Pandoc.AlignLeft = PP.AlignLeft + align Pandoc.AlignCenter = PP.AlignCenter + align Pandoc.AlignDefault = PP.AlignLeft + align Pandoc.AlignRight = PP.AlignRight + +prettyBlock theme (Pandoc.Div _attrs blocks) = prettyBlocks theme blocks + +prettyBlock _theme Pandoc.Null = mempty + +#if MIN_VERSION_pandoc(1,18,0) +-- 'LineBlock' elements are new in pandoc-1.18 +prettyBlock theme@Theme {..} (Pandoc.LineBlock inliness) = + let ind = PP.NotTrimmable (themed themeLineBlock "| ") in + PP.wrapAt Nothing $ + PP.indent ind ind $ + PP.vcat $ + map (prettyInlines theme) inliness +#endif + + +-------------------------------------------------------------------------------- +prettyBlocks :: Theme -> [Pandoc.Block] -> PP.Doc +prettyBlocks theme = PP.vcat . map (prettyBlock theme) + + +-------------------------------------------------------------------------------- +prettyInline :: Theme -> Pandoc.Inline -> PP.Doc + +prettyInline _theme Pandoc.Space = PP.space + +prettyInline _theme (Pandoc.Str str) = PP.string str + +prettyInline theme@Theme {..} (Pandoc.Emph inlines) = + themed themeEmph $ + prettyInlines theme inlines + +prettyInline theme@Theme {..} (Pandoc.Strong inlines) = + themed themeStrong $ + prettyInlines theme inlines + +prettyInline Theme {..} (Pandoc.Code _ txt) = + themed themeCode $ + " " <> PP.string txt <> " " + +prettyInline theme@Theme {..} link@(Pandoc.Link _attrs text (target, _title)) + | isReferenceLink link = + "[" <> themed themeLinkText (prettyInlines theme text) <> "]" + | otherwise = + "<" <> themed themeLinkTarget (PP.string target) <> ">" + +prettyInline _theme Pandoc.SoftBreak = PP.softline + +prettyInline _theme Pandoc.LineBreak = PP.hardline + +prettyInline theme@Theme {..} (Pandoc.Strikeout t) = + "~~" <> themed themeStrikeout (prettyInlines theme t) <> "~~" + +prettyInline theme@Theme {..} (Pandoc.Quoted Pandoc.SingleQuote t) = + "'" <> themed themeQuoted (prettyInlines theme t) <> "'" +prettyInline theme@Theme {..} (Pandoc.Quoted Pandoc.DoubleQuote t) = + "'" <> themed themeQuoted (prettyInlines theme t) <> "'" + +prettyInline Theme {..} (Pandoc.Math _ t) = + themed themeMath (PP.string t) + +prettyInline theme@Theme {..} (Pandoc.Image _attrs text (target, _title)) = + "![" <> themed themeImageText (prettyInlines theme text) <> "](" <> + themed themeImageTarget (PP.string target) <> ")" + +-- These elements aren't really supported. +prettyInline theme (Pandoc.Cite _ t) = prettyInlines theme t +prettyInline theme (Pandoc.Span _ t) = prettyInlines theme t +prettyInline _theme (Pandoc.RawInline _ t) = PP.string t +prettyInline theme (Pandoc.Note t) = prettyBlocks theme t +prettyInline theme (Pandoc.Superscript t) = prettyInlines theme t +prettyInline theme (Pandoc.Subscript t) = prettyInlines theme t +prettyInline theme (Pandoc.SmallCaps t) = prettyInlines theme t +-- prettyInline unsupported = PP.ondullred $ PP.string $ show unsupported + + +-------------------------------------------------------------------------------- +prettyInlines :: Theme -> [Pandoc.Inline] -> PP.Doc +prettyInlines theme = mconcat . map (prettyInline theme) + + +-------------------------------------------------------------------------------- +prettyReferences :: Theme -> Slide -> [PP.Doc] +prettyReferences theme@Theme {..} = + map prettyReference . getReferences . unSlide + where + getReferences :: [Pandoc.Block] -> [Pandoc.Inline] + getReferences = filter isReferenceLink . grecQ + + prettyReference :: Pandoc.Inline -> PP.Doc + prettyReference (Pandoc.Link _attrs text (target, title)) = + "[" <> + themed themeLinkText (prettyInlines theme $ Pandoc.newlineToSpace text) <> + "](" <> + themed themeLinkTarget (PP.string target) <> + (if null title + then mempty + else PP.space <> "\"" <> PP.string title <> "\"") + <> ")" + prettyReference _ = mempty + + +-------------------------------------------------------------------------------- +isReferenceLink :: Pandoc.Inline -> Bool +isReferenceLink (Pandoc.Link _attrs text (target, _)) = + [Pandoc.Str target] /= text +isReferenceLink _ = False diff --git a/src/Patat/Presentation/Display/CodeBlock.hs b/src/Patat/Presentation/Display/CodeBlock.hs new file mode 100644 index 0000000..4888166 --- /dev/null +++ b/src/Patat/Presentation/Display/CodeBlock.hs @@ -0,0 +1,79 @@ +-------------------------------------------------------------------------------- +-- | Displaying code blocks, optionally with syntax highlighting. +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE RecordWildCards #-} +module Patat.Presentation.Display.CodeBlock + ( prettyCodeBlock + ) where + + +-------------------------------------------------------------------------------- +import Data.Char (toLower) +import Data.List (find) +import Data.Monoid (mconcat, (<>)) +import qualified Data.Set as S +import Patat.Presentation.Display.Table (themed) +import qualified Patat.PrettyPrint as PP +import Patat.Theme +import qualified Text.Highlighting.Kate as Kate +import Prelude + + +-------------------------------------------------------------------------------- +lower :: String -> String +lower = map toLower + + +-------------------------------------------------------------------------------- +supportedLanguages :: S.Set String +supportedLanguages = S.fromList (map lower Kate.languages) + + +-------------------------------------------------------------------------------- +highlight :: [String] -> String -> [Kate.SourceLine] +highlight classes rawCodeBlock = + case find (\c -> lower c `S.member` supportedLanguages) classes of + Nothing -> zeroHighlight rawCodeBlock + Just lang -> Kate.highlightAs lang rawCodeBlock + + +-------------------------------------------------------------------------------- +-- | This does fake highlighting, everything becomes a normal token. That makes +-- things a bit easier, since we only need to deal with one cases in the +-- renderer. +zeroHighlight :: String -> [Kate.SourceLine] +zeroHighlight str = [[(Kate.NormalTok, line)] | line <- lines str] + + +-------------------------------------------------------------------------------- +prettyCodeBlock :: Theme -> [String] -> String -> PP.Doc +prettyCodeBlock theme@Theme {..} classes rawCodeBlock = + PP.vcat (map blockified sourceLines) <> + PP.hardline + where + sourceLines :: [Kate.SourceLine] + sourceLines = + [[]] ++ highlight classes rawCodeBlock ++ [[]] + + prettySourceLine :: Kate.SourceLine -> PP.Doc + prettySourceLine = mconcat . map prettyToken + + prettyToken :: Kate.Token -> PP.Doc + prettyToken (tokenType, str) = + themed (syntaxHighlight theme tokenType) (PP.string str) + + sourceLineLength :: Kate.SourceLine -> Int + sourceLineLength line = sum [length str | (_, str) <- line] + + blockWidth :: Int + blockWidth = foldr max 0 (map sourceLineLength sourceLines) + + blockified :: Kate.SourceLine -> PP.Doc + blockified line = + let len = sourceLineLength line + indent = PP.NotTrimmable " " in + PP.indent indent indent $ + themed themeCodeBlock $ + " " <> + prettySourceLine line <> + PP.string (replicate (blockWidth - len) ' ') <> " " diff --git a/src/Patat/Presentation/Display/Table.hs b/src/Patat/Presentation/Display/Table.hs new file mode 100644 index 0000000..fee68c9 --- /dev/null +++ b/src/Patat/Presentation/Display/Table.hs @@ -0,0 +1,107 @@ +-------------------------------------------------------------------------------- +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE RecordWildCards #-} +module Patat.Presentation.Display.Table + ( Table (..) + , prettyTable + + , themed + ) where + + +-------------------------------------------------------------------------------- +import Data.List (intersperse, transpose) +import Data.Monoid (mconcat, mempty, (<>)) +import Patat.PrettyPrint ((<$$>)) +import qualified Patat.PrettyPrint as PP +import Patat.Theme (Theme (..)) +import qualified Patat.Theme as Theme +import Prelude + + +-------------------------------------------------------------------------------- +data Table = Table + { tCaption :: PP.Doc + , tAligns :: [PP.Alignment] + , tHeaders :: [PP.Doc] + , tRows :: [[PP.Doc]] + } + + +-------------------------------------------------------------------------------- +prettyTable + :: Theme -> Table -> PP.Doc +prettyTable theme@Theme {..} Table {..} = + PP.indent (PP.Trimmable " ") (PP.Trimmable " ") $ + lineIf (not isHeaderLess) (hcat2 headerHeight + [ themed themeTableHeader (PP.align w a (vpad headerHeight header)) + | (w, a, header) <- zip3 columnWidths tAligns tHeaders + ]) <> + dashedHeaderSeparator theme columnWidths <$$> + joinRows + [ hcat2 rowHeight + [ PP.align w a (vpad rowHeight cell) + | (w, a, cell) <- zip3 columnWidths tAligns row + ] + | (rowHeight, row) <- zip rowHeights tRows + ] <$$> + lineIf isHeaderLess (dashedHeaderSeparator theme columnWidths) <> + lineIf + (not $ PP.null tCaption) (PP.hardline <> "Table: " <> tCaption) + where + lineIf cond line = if cond then line <> PP.hardline else mempty + + joinRows + | all (all isSimpleCell) tRows = PP.vcat + | otherwise = PP.vcat . intersperse "" + + isHeaderLess = all PP.null tHeaders + + headerDimensions = map PP.dimensions tHeaders :: [(Int, Int)] + rowDimensions = map (map PP.dimensions) tRows :: [[(Int, Int)]] + + columnWidths :: [Int] + columnWidths = + [ safeMax (map snd col) + | col <- transpose (headerDimensions : rowDimensions) + ] + + rowHeights = map (safeMax . map fst) rowDimensions :: [Int] + headerHeight = safeMax (map fst headerDimensions) :: Int + + vpad :: Int -> PP.Doc -> PP.Doc + vpad height doc = + let (actual, _) = PP.dimensions doc in + doc <> mconcat (replicate (height - actual) PP.hardline) + + safeMax = foldr max 0 + + hcat2 :: Int -> [PP.Doc] -> PP.Doc + hcat2 rowHeight = PP.paste . intersperse (spaces2 rowHeight) + + spaces2 :: Int -> PP.Doc + spaces2 rowHeight = + mconcat $ intersperse PP.hardline $ + replicate rowHeight (PP.string " ") + + +-------------------------------------------------------------------------------- +isSimpleCell :: PP.Doc -> Bool +isSimpleCell = (<= 1) . fst . PP.dimensions + + +-------------------------------------------------------------------------------- +dashedHeaderSeparator :: Theme -> [Int] -> PP.Doc +dashedHeaderSeparator Theme {..} columnWidths = + mconcat $ intersperse (PP.string " ") + [ themed themeTableSeparator (PP.string (replicate w '-')) + | w <- columnWidths + ] + + +-------------------------------------------------------------------------------- +-- | This does not really belong in the module. +themed :: Maybe Theme.Style -> PP.Doc -> PP.Doc +themed Nothing = id +themed (Just (Theme.Style [])) = id +themed (Just (Theme.Style codes)) = PP.ansi codes diff --git a/src/Patat/Presentation/Interactive.hs b/src/Patat/Presentation/Interactive.hs new file mode 100644 index 0000000..226a715 --- /dev/null +++ b/src/Patat/Presentation/Interactive.hs @@ -0,0 +1,100 @@ +-------------------------------------------------------------------------------- +-- | Module that allows the user to interact with the presentation +{-# LANGUAGE GeneralizedNewtypeDeriving #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE RecordWildCards #-} +module Patat.Presentation.Interactive + ( PresentationCommand (..) + , readPresentationCommand + + , UpdatedPresentation (..) + , updatePresentation + ) where + + +-------------------------------------------------------------------------------- +import Patat.Presentation.Internal +import Patat.Presentation.Read + + +-------------------------------------------------------------------------------- +data PresentationCommand + = Exit + | Forward + | Backward + | SkipForward + | SkipBackward + | First + | Last + | Reload + + +-------------------------------------------------------------------------------- +readPresentationCommand :: IO PresentationCommand +readPresentationCommand = do + k <- readKey + case k of + "q" -> return Exit + "\n" -> return Forward + "\DEL" -> return Backward + "h" -> return Backward + "j" -> return SkipForward + "k" -> return SkipBackward + "l" -> return Forward + "\ESC[C" -> return Forward + "\ESC[D" -> return Backward + "\ESC[B" -> return SkipForward + "\ESC[A" -> return SkipBackward + "0" -> return First + "G" -> return Last + "r" -> return Reload + _ -> readPresentationCommand + where + readKey :: IO String + readKey = do + c0 <- getChar + case c0 of + '\ESC' -> do + c1 <- getChar + case c1 of + '[' -> do + c2 <- getChar + return [c0, c1, c2] + _ -> return [c0, c1] + _ -> return [c0] + + +-------------------------------------------------------------------------------- +data UpdatedPresentation + = UpdatedPresentation !Presentation + | ExitedPresentation + | ErroredPresentation String + deriving (Show) + + +-------------------------------------------------------------------------------- +updatePresentation + :: PresentationCommand -> Presentation -> IO UpdatedPresentation + +updatePresentation cmd presentation = case cmd of + Exit -> return ExitedPresentation + Forward -> return $ goToSlide (\x -> x + 1) + Backward -> return $ goToSlide (\x -> x - 1) + SkipForward -> return $ goToSlide (\x -> x + 10) + SkipBackward -> return $ goToSlide (\x -> x - 10) + First -> return $ goToSlide (\_ -> 0) + Last -> return $ goToSlide (\_ -> numSlides presentation - 1) + Reload -> reloadPresentation + where + numSlides pres = length (pSlides pres) + clip idx pres = min (max 0 idx) (numSlides pres - 1) + + goToSlide f = UpdatedPresentation $ + presentation {pActiveSlide = clip (f $ pActiveSlide presentation) presentation} + + reloadPresentation = do + errOrPres <- readPresentation (pFilePath presentation) + return $ case errOrPres of + Left err -> ErroredPresentation err + Right pres -> UpdatedPresentation $ + pres {pActiveSlide = clip (pActiveSlide presentation) pres} diff --git a/src/Patat/Presentation/Internal.hs b/src/Patat/Presentation/Internal.hs new file mode 100644 index 0000000..f11c46b --- /dev/null +++ b/src/Patat/Presentation/Internal.hs @@ -0,0 +1,71 @@ +-------------------------------------------------------------------------------- +{-# LANGUAGE GeneralizedNewtypeDeriving #-} +{-# LANGUAGE TemplateHaskell #-} +module Patat.Presentation.Internal + ( Presentation (..) + , PresentationSettings (..) + , defaultPresentationSettings + , Slide (..) + ) where + + +-------------------------------------------------------------------------------- +import Control.Monad (mplus) +import qualified Data.Aeson.Extended as A +import qualified Data.Aeson.TH.Extended as A +import Data.Monoid (Monoid (..)) +import qualified Patat.Theme as Theme +import qualified Text.Pandoc as Pandoc +import Prelude + + +-------------------------------------------------------------------------------- +data Presentation = Presentation + { pFilePath :: !FilePath + , pTitle :: ![Pandoc.Inline] + , pAuthor :: ![Pandoc.Inline] + , pSettings :: !PresentationSettings + , pSlides :: [Slide] + , pActiveSlide :: !Int + } deriving (Show) + + +-------------------------------------------------------------------------------- +-- | These are patat-specific settings. That is where they differ from more +-- general metadata (author, title...) +data PresentationSettings = PresentationSettings + { psRows :: !(Maybe (A.FlexibleNum Int)) + , psColumns :: !(Maybe (A.FlexibleNum Int)) + , psWrap :: !(Maybe Bool) + , psTheme :: !(Maybe Theme.Theme) + } deriving (Show) + + +-------------------------------------------------------------------------------- +instance Monoid PresentationSettings where + mempty = PresentationSettings Nothing Nothing Nothing Nothing + mappend l r = PresentationSettings + { psRows = psRows l `mplus` psRows r + , psColumns = psColumns l `mplus` psColumns r + , psWrap = psWrap l `mplus` psWrap r + , psTheme = psTheme l `mappend` psTheme r + } + + +-------------------------------------------------------------------------------- +defaultPresentationSettings :: PresentationSettings +defaultPresentationSettings = PresentationSettings + { psRows = Nothing + , psColumns = Nothing + , psWrap = Nothing + , psTheme = Just Theme.defaultTheme + } + + +-------------------------------------------------------------------------------- +newtype Slide = Slide {unSlide :: [Pandoc.Block]} + deriving (Monoid, Show) + + +-------------------------------------------------------------------------------- +$(A.deriveJSON A.dropPrefixOptions ''PresentationSettings) diff --git a/src/Patat/Presentation/Read.hs b/src/Patat/Presentation/Read.hs new file mode 100644 index 0000000..c962632 --- /dev/null +++ b/src/Patat/Presentation/Read.hs @@ -0,0 +1,121 @@ +-- | Read a presentation from disk. +{-# LANGUAGE BangPatterns #-} +{-# LANGUAGE RecordWildCards #-} +module Patat.Presentation.Read + ( readPresentation + ) where + + +-------------------------------------------------------------------------------- +import Control.Monad.Except (ExceptT (..), runExceptT, + throwError) +import Control.Monad.Trans (liftIO) +import qualified Data.Aeson as A +import qualified Data.ByteString as B +import Data.Monoid (mempty, (<>)) +import qualified Data.Set as Set +import qualified Data.Yaml as Yaml +import Patat.Presentation.Internal +import System.Directory (doesFileExist, getHomeDirectory) +import System.FilePath (takeExtension, ()) +import qualified Text.Pandoc.Error as Pandoc +import qualified Text.Pandoc.Extended as Pandoc +import Prelude + + +-------------------------------------------------------------------------------- +readPresentation :: FilePath -> IO (Either String Presentation) +readPresentation filePath = runExceptT $ do + src <- liftIO $ readFile filePath + reader <- case readExtension ext of + Nothing -> throwError $ "Unknown file extension: " ++ show ext + Just x -> return x + doc@(Pandoc.Pandoc meta _) <- case reader src of + Left e -> throwError $ "Could not parse document: " ++ show e + Right x -> return x + + homeSettings <- ExceptT readHomeSettings + metaSettings <- ExceptT $ return $ readMetaSettings meta + let settings = metaSettings <> homeSettings <> defaultPresentationSettings + + ExceptT $ return $ pandocToPresentation filePath settings doc + where + ext = takeExtension filePath + + +-------------------------------------------------------------------------------- +readExtension + :: String -> Maybe (String -> Either Pandoc.PandocError Pandoc.Pandoc) +readExtension fileExt = case fileExt of + ".md" -> Just $ Pandoc.readMarkdown Pandoc.def + ".lhs" -> Just $ Pandoc.readMarkdown lhsOpts + "" -> Just $ Pandoc.readMarkdown Pandoc.def + ".org" -> Just $ Pandoc.readOrg Pandoc.def + _ -> Nothing + + where + lhsOpts = Pandoc.def + { Pandoc.readerExtensions = Set.insert Pandoc.Ext_literate_haskell + (Pandoc.readerExtensions Pandoc.def) + } + + +-------------------------------------------------------------------------------- +pandocToPresentation + :: FilePath -> PresentationSettings -> Pandoc.Pandoc + -> Either String Presentation +pandocToPresentation pFilePath pSettings pandoc@(Pandoc.Pandoc meta _) = do + let !pTitle = Pandoc.docTitle meta + !pSlides = pandocToSlides pandoc + !pActiveSlide = 0 + !pAuthor = concat (Pandoc.docAuthors meta) + return Presentation {..} + + +-------------------------------------------------------------------------------- +-- | Read settings from the metadata block in the Pandoc document. +readMetaSettings :: Pandoc.Meta -> Either String PresentationSettings +readMetaSettings meta = case Pandoc.lookupMeta "patat" meta of + Nothing -> return mempty + Just val -> resultToEither $! A.fromJSON $! Pandoc.metaToJson val + where + resultToEither :: A.Result a -> Either String a + resultToEither (A.Success x) = Right x + resultToEither (A.Error e) = Left $! + "Error parsing patat settings from metadata: " ++ e + + +-------------------------------------------------------------------------------- +-- | Read settings from "$HOME/.patat.yaml". +readHomeSettings :: IO (Either String PresentationSettings) +readHomeSettings = do + home <- getHomeDirectory + let path = home ".patat.yaml" + exists <- doesFileExist path + if not exists + then return (Right mempty) + else do + contents <- B.readFile path + return $! Yaml.decodeEither contents + + +-------------------------------------------------------------------------------- +-- | Split a pandoc document into slides. If the document contains horizonal +-- rules, we use those as slide delimiters. If there are no horizontal rules, +-- we split using h1 headers. +pandocToSlides :: Pandoc.Pandoc -> [Slide] +pandocToSlides (Pandoc.Pandoc _meta blocks0) + | any (== Pandoc.HorizontalRule) blocks0 = splitAtRules blocks0 + | otherwise = splitAtH1s blocks0 + where + splitAtRules blocks = case break (== Pandoc.HorizontalRule) blocks of + (xs, []) -> [Slide xs] + (xs, (_rule : ys)) -> Slide xs : splitAtRules ys + + splitAtH1s [] = [] + splitAtH1s (b : bs) = case break isH1 bs of + (xs, []) -> [Slide (b : xs)] + (xs, (y : ys)) -> Slide (b : xs) : splitAtH1s (y : ys) + + isH1 (Pandoc.Header i _ _) = i == 1 + isH1 _ = False diff --git a/src/Patat/PrettyPrint.hs b/src/Patat/PrettyPrint.hs new file mode 100644 index 0000000..7b24b37 --- /dev/null +++ b/src/Patat/PrettyPrint.hs @@ -0,0 +1,404 @@ +-------------------------------------------------------------------------------- +-- | This is a small pretty-printing library. +{-# LANGUAGE DeriveFoldable #-} +{-# LANGUAGE DeriveFunctor #-} +{-# LANGUAGE DeriveTraversable #-} +{-# LANGUAGE GeneralizedNewtypeDeriving #-} +{-# LANGUAGE RecordWildCards #-} +module Patat.PrettyPrint + ( Doc + , toString + , dimensions + , null + + , hPutDoc + , putDoc + + , string + , text + , space + , softline + , hardline + + , wrapAt + + , Trimmable (..) + , indent + + , ansi + + , (<+>) + , (<$$>) + , vcat + + -- * Exotic combinators + , Alignment (..) + , align + , paste + ) where + + +-------------------------------------------------------------------------------- +import Control.Monad.Reader (asks, local) +import Control.Monad.RWS (RWS, runRWS) +import Control.Monad.State (get, gets, modify) +import Control.Monad.Writer (tell) +import Data.Foldable (Foldable) +import qualified Data.List as L +import Data.Monoid (Monoid, mconcat, mempty, (<>)) +import Data.String (IsString (..)) +import qualified Data.Text as T +import Data.Traversable (Traversable, traverse) +import qualified System.Console.ANSI as Ansi +import qualified System.IO as IO +import Prelude hiding (null) + + +-------------------------------------------------------------------------------- +-- | A simple chunk of text. All ANSI codes are "reset" after printing. +data Chunk + = StringChunk [Ansi.SGR] String + | NewlineChunk + deriving (Eq) + + +-------------------------------------------------------------------------------- +type Chunks = [Chunk] + + +-------------------------------------------------------------------------------- +hPutChunk :: IO.Handle -> Chunk -> IO () +hPutChunk h NewlineChunk = IO.hPutStrLn h "" +hPutChunk h (StringChunk codes str) = do + Ansi.hSetSGR h (reverse codes) + IO.hPutStr h str + Ansi.hSetSGR h [Ansi.Reset] + + +-------------------------------------------------------------------------------- +chunkToString :: Chunk -> String +chunkToString NewlineChunk = "\n" +chunkToString (StringChunk _ str) = str + + +-------------------------------------------------------------------------------- +-- | If two neighboring chunks have the same set of ANSI codes, we can group +-- them together. +optimizeChunks :: Chunks -> Chunks +optimizeChunks (StringChunk c1 s1 : StringChunk c2 s2 : chunks) + | c1 == c2 = optimizeChunks (StringChunk c1 (s1 <> s2) : chunks) + | otherwise = + StringChunk c1 s1 : optimizeChunks (StringChunk c2 s2 : chunks) +optimizeChunks (x : chunks) = x : optimizeChunks chunks +optimizeChunks [] = [] + + +-------------------------------------------------------------------------------- +chunkLines :: Chunks -> [Chunks] +chunkLines chunks = case break (== NewlineChunk) chunks of + (xs, _newline : ys) -> xs : chunkLines ys + (xs, []) -> [xs] + + +-------------------------------------------------------------------------------- +data DocE + = String String + | Softspace + | Hardspace + | Softline + | Hardline + | WrapAt + { wrapAtCol :: Maybe Int + , wrapDoc :: Doc + } + | Ansi + { ansiCode :: [Ansi.SGR] -> [Ansi.SGR] -- ^ Modifies current codes. + , ansiDoc :: Doc + } + | Indent + { indentFirstLine :: LineBuffer + , indentOtherLines :: LineBuffer + , indentDoc :: Doc + } + + +-------------------------------------------------------------------------------- +chunkToDocE :: Chunk -> DocE +chunkToDocE NewlineChunk = Hardline +chunkToDocE (StringChunk codes str) = Ansi (\_ -> codes) (Doc [String str]) + + +-------------------------------------------------------------------------------- +newtype Doc = Doc {unDoc :: [DocE]} + deriving (Monoid) + + +-------------------------------------------------------------------------------- +instance IsString Doc where + fromString = string + + +-------------------------------------------------------------------------------- +instance Show Doc where + show = toString + + +-------------------------------------------------------------------------------- +data DocEnv = DocEnv + { deCodes :: [Ansi.SGR] -- ^ Most recent ones first in the list + , deIndent :: LineBuffer -- ^ Don't need to store first-line indent + , deWrap :: Maybe Int -- ^ Wrap at columns + } + + +-------------------------------------------------------------------------------- +type DocM = RWS DocEnv Chunks LineBuffer + + +-------------------------------------------------------------------------------- +data Trimmable a + = NotTrimmable !a + | Trimmable !a + deriving (Foldable, Functor, Traversable) + + +-------------------------------------------------------------------------------- +-- | Note that this is reversed so we have fast append +type LineBuffer = [Trimmable Chunk] + + +-------------------------------------------------------------------------------- +bufferToChunks :: LineBuffer -> Chunks +bufferToChunks = map trimmableToChunk . reverse . dropWhile isTrimmable + where + isTrimmable (NotTrimmable _) = False + isTrimmable (Trimmable _) = True + + trimmableToChunk (NotTrimmable c) = c + trimmableToChunk (Trimmable c) = c + + +-------------------------------------------------------------------------------- +docToChunks :: Doc -> Chunks +docToChunks doc0 = + let env0 = DocEnv [] [] Nothing + ((), b, cs) = runRWS (go $ unDoc doc0) env0 mempty in + optimizeChunks (cs <> bufferToChunks b) + where + go :: [DocE] -> DocM () + + go [] = return () + + go (String str : docs) = do + chunk <- makeChunk str + modify (NotTrimmable chunk :) + go docs + + go (Softspace : docs) = do + hard <- softConversion Softspace docs + go (hard : docs) + + go (Hardspace : docs) = do + chunk <- makeChunk " " + modify (NotTrimmable chunk :) + go docs + + go (Softline : docs) = do + hard <- softConversion Softline docs + go (hard : docs) + + go (Hardline : docs) = do + buffer <- get + tell $ bufferToChunks buffer <> [NewlineChunk] + indentation <- asks deIndent + modify $ \_ -> if L.null docs then [] else indentation + go docs + + go (WrapAt {..} : docs) = do + local (\env -> env {deWrap = wrapAtCol}) $ go (unDoc wrapDoc) + go docs + + go (Ansi {..} : docs) = do + local (\env -> env {deCodes = ansiCode (deCodes env)}) $ + go (unDoc ansiDoc) + go docs + + go (Indent {..} : docs) = do + local (\env -> env {deIndent = indentOtherLines ++ deIndent env}) $ do + modify (indentFirstLine ++) + go (unDoc indentDoc) + go docs + + makeChunk :: String -> DocM Chunk + makeChunk str = do + codes <- asks deCodes + return $ StringChunk codes str + + -- Convert 'Softspace' or 'Softline' to 'Hardspace' or 'Hardline' + softConversion :: DocE -> [DocE] -> DocM DocE + softConversion soft docs = do + mbWrapCol <- asks deWrap + case mbWrapCol of + Nothing -> return hard + Just maxCol -> do + -- Slow. + currentLine <- gets (concatMap chunkToString . bufferToChunks) + let currentCol = length currentLine + case nextWordLength docs of + Nothing -> return hard + Just l + | currentCol + 1 + l <= maxCol -> return Hardspace + | otherwise -> return Hardline + where + hard = case soft of + Softspace -> Hardspace + Softline -> Hardline + _ -> soft + + nextWordLength :: [DocE] -> Maybe Int + nextWordLength [] = Nothing + nextWordLength (String x : xs) + | L.null x = nextWordLength xs + | otherwise = Just (length x) + nextWordLength (Softspace : xs) = nextWordLength xs + nextWordLength (Hardspace : xs) = nextWordLength xs + nextWordLength (Softline : xs) = nextWordLength xs + nextWordLength (Hardline : _) = Nothing + nextWordLength (WrapAt {..} : xs) = nextWordLength (unDoc wrapDoc ++ xs) + nextWordLength (Ansi {..} : xs) = nextWordLength (unDoc ansiDoc ++ xs) + nextWordLength (Indent {..} : xs) = nextWordLength (unDoc indentDoc ++ xs) + + +-------------------------------------------------------------------------------- +toString :: Doc -> String +toString = concat . map chunkToString . docToChunks + + +-------------------------------------------------------------------------------- +-- | Returns the rows and columns necessary to render this document +dimensions :: Doc -> (Int, Int) +dimensions doc = + let ls = lines (toString doc) in + (length ls, foldr max 0 (map length ls)) + + +-------------------------------------------------------------------------------- +null :: Doc -> Bool +null doc = case unDoc doc of [] -> True; _ -> False + + +-------------------------------------------------------------------------------- +hPutDoc :: IO.Handle -> Doc -> IO () +hPutDoc h = mapM_ (hPutChunk h) . docToChunks + + +-------------------------------------------------------------------------------- +putDoc :: Doc -> IO () +putDoc = hPutDoc IO.stdout + + +-------------------------------------------------------------------------------- +mkDoc :: DocE -> Doc +mkDoc e = Doc [e] + + +-------------------------------------------------------------------------------- +string :: String -> Doc +string = mkDoc . String -- TODO (jaspervdj): Newline conversion + + +-------------------------------------------------------------------------------- +text :: T.Text -> Doc +text = string . T.unpack + + +-------------------------------------------------------------------------------- +space :: Doc +space = mkDoc Softspace + + +-------------------------------------------------------------------------------- +softline :: Doc +softline = mkDoc Softline + + +-------------------------------------------------------------------------------- +hardline :: Doc +hardline = mkDoc Hardline + + +-------------------------------------------------------------------------------- +wrapAt :: Maybe Int -> Doc -> Doc +wrapAt wrapAtCol wrapDoc = mkDoc WrapAt {..} + + +-------------------------------------------------------------------------------- +indent :: Trimmable Doc -> Trimmable Doc -> Doc -> Doc +indent firstLineDoc otherLinesDoc doc = mkDoc $ Indent + { indentFirstLine = traverse docToChunks firstLineDoc + , indentOtherLines = traverse docToChunks otherLinesDoc + , indentDoc = doc + } + + +-------------------------------------------------------------------------------- +ansi :: [Ansi.SGR] -> Doc -> Doc +ansi codes = mkDoc . Ansi (codes ++) + + +-------------------------------------------------------------------------------- +(<+>) :: Doc -> Doc -> Doc +x <+> y = x <> space <> y +infixr 6 <+> + + +-------------------------------------------------------------------------------- +(<$$>) :: Doc -> Doc -> Doc +x <$$> y = x <> hardline <> y +infixr 5 <$$> + + +-------------------------------------------------------------------------------- +vcat :: [Doc] -> Doc +vcat = mconcat . L.intersperse hardline + + +-------------------------------------------------------------------------------- +data Alignment = AlignLeft | AlignCenter | AlignRight deriving (Eq, Ord, Show) + + +-------------------------------------------------------------------------------- +align :: Int -> Alignment -> Doc -> Doc +align width alignment doc0 = + let chunks0 = docToChunks doc0 + lines_ = chunkLines chunks0 in + vcat + [ Doc (map chunkToDocE (alignLine line)) + | line <- lines_ + ] + where + lineWidth :: [Chunk] -> Int + lineWidth = sum . map (length . chunkToString) + + alignLine :: [Chunk] -> [Chunk] + alignLine line = + let actual = lineWidth line + spaces n = [StringChunk [] (replicate n ' ')] in + case alignment of + AlignLeft -> line <> spaces (width - actual) + AlignRight -> spaces (width - actual) <> line + AlignCenter -> + let r = (width - actual) `div` 2 + l = (width - actual) - r in + spaces l <> line <> spaces r + + +-------------------------------------------------------------------------------- +-- | Like the unix program 'paste'. +paste :: [Doc] -> Doc +paste docs0 = + let chunkss = map docToChunks docs0 :: [Chunks] + cols = map chunkLines chunkss :: [[Chunks]] + rows0 = L.transpose cols :: [[Chunks]] + rows1 = map (map (Doc . map chunkToDocE)) rows0 :: [[Doc]] in + vcat $ map mconcat rows1 diff --git a/src/Patat/Theme.hs b/src/Patat/Theme.hs new file mode 100644 index 0000000..706f825 --- /dev/null +++ b/src/Patat/Theme.hs @@ -0,0 +1,286 @@ +-------------------------------------------------------------------------------- +{-# LANGUAGE GeneralizedNewtypeDeriving #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE TemplateHaskell #-} +module Patat.Theme + ( Theme (..) + , defaultTheme + + , Style (..) + + , SyntaxHighlighting (..) + , defaultSyntaxHighlighting + , syntaxHighlight + ) where + + +-------------------------------------------------------------------------------- +import Control.Monad (forM_, mplus) +import qualified Data.Aeson as A +import qualified Data.Aeson.TH.Extended as A +import Data.Char (toLower, toUpper) +import Data.List (intercalate, isSuffixOf) +import qualified Data.Map as M +import Data.Maybe (mapMaybe, maybeToList) +import Data.Monoid (Monoid (..), (<>)) +import qualified Data.Text as T +import qualified System.Console.ANSI as Ansi +import qualified Text.Highlighting.Kate as Kate +import Text.Read (readMaybe) +import Prelude + + +-------------------------------------------------------------------------------- +data Theme = Theme + { themeBorders :: !(Maybe Style) + , themeHeader :: !(Maybe Style) + , themeCodeBlock :: !(Maybe Style) + , themeBulletList :: !(Maybe Style) + , themeBulletListMarkers :: !(Maybe T.Text) + , themeOrderedList :: !(Maybe Style) + , themeBlockQuote :: !(Maybe Style) + , themeDefinitionTerm :: !(Maybe Style) + , themeDefinitionList :: !(Maybe Style) + , themeTableHeader :: !(Maybe Style) + , themeTableSeparator :: !(Maybe Style) + , themeLineBlock :: !(Maybe Style) + , themeEmph :: !(Maybe Style) + , themeStrong :: !(Maybe Style) + , themeCode :: !(Maybe Style) + , themeLinkText :: !(Maybe Style) + , themeLinkTarget :: !(Maybe Style) + , themeStrikeout :: !(Maybe Style) + , themeQuoted :: !(Maybe Style) + , themeMath :: !(Maybe Style) + , themeImageText :: !(Maybe Style) + , themeImageTarget :: !(Maybe Style) + , themeSyntaxHighlighting :: !(Maybe SyntaxHighlighting) + } deriving (Show) + + +-------------------------------------------------------------------------------- +instance Monoid Theme where + mempty = Theme + Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing + Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing + Nothing Nothing Nothing Nothing Nothing + + mappend l r = Theme + { themeBorders = mplusOn themeBorders + , themeHeader = mplusOn themeHeader + , themeCodeBlock = mplusOn themeCodeBlock + , themeBulletList = mplusOn themeBulletList + , themeBulletListMarkers = mplusOn themeBulletListMarkers + , themeOrderedList = mplusOn themeOrderedList + , themeBlockQuote = mplusOn themeBlockQuote + , themeDefinitionTerm = mplusOn themeDefinitionTerm + , themeDefinitionList = mplusOn themeDefinitionList + , themeTableHeader = mplusOn themeTableHeader + , themeTableSeparator = mplusOn themeTableSeparator + , themeLineBlock = mplusOn themeLineBlock + , themeEmph = mplusOn themeEmph + , themeStrong = mplusOn themeStrong + , themeCode = mplusOn themeCode + , themeLinkText = mplusOn themeLinkText + , themeLinkTarget = mplusOn themeLinkTarget + , themeStrikeout = mplusOn themeStrikeout + , themeQuoted = mplusOn themeQuoted + , themeMath = mplusOn themeMath + , themeImageText = mplusOn themeImageText + , themeImageTarget = mplusOn themeImageTarget + , themeSyntaxHighlighting = mappendOn themeSyntaxHighlighting + } + where + mplusOn f = f l `mplus` f r + mappendOn f = f l `mappend` f r + + +-------------------------------------------------------------------------------- +defaultTheme :: Theme +defaultTheme = Theme + { themeBorders = dull Ansi.Yellow + , themeHeader = dull Ansi.Blue + , themeCodeBlock = dull Ansi.White <> ondull Ansi.Black + , themeBulletList = dull Ansi.Magenta + , themeBulletListMarkers = Just "-*" + , themeOrderedList = dull Ansi.Magenta + , themeBlockQuote = dull Ansi.Green + , themeDefinitionTerm = dull Ansi.Blue + , themeDefinitionList = dull Ansi.Magenta + , themeTableHeader = dull Ansi.Blue + , themeTableSeparator = dull Ansi.Magenta + , themeLineBlock = dull Ansi.Magenta + , themeEmph = dull Ansi.Green + , themeStrong = dull Ansi.Red <> bold + , themeCode = dull Ansi.White <> ondull Ansi.Black + , themeLinkText = dull Ansi.Green + , themeLinkTarget = dull Ansi.Cyan <> underline + , themeStrikeout = ondull Ansi.Red + , themeQuoted = dull Ansi.Green + , themeMath = dull Ansi.Green + , themeImageText = dull Ansi.Green + , themeImageTarget = dull Ansi.Cyan <> underline + , themeSyntaxHighlighting = Just defaultSyntaxHighlighting + } + where + dull c = Just $ Style [Ansi.SetColor Ansi.Foreground Ansi.Dull c] + ondull c = Just $ Style [Ansi.SetColor Ansi.Background Ansi.Dull c] + bold = Just $ Style [Ansi.SetConsoleIntensity Ansi.BoldIntensity] + underline = Just $ Style [Ansi.SetUnderlining Ansi.SingleUnderline] + + +-------------------------------------------------------------------------------- +newtype Style = Style {unStyle :: [Ansi.SGR]} + deriving (Monoid, Show) + + +-------------------------------------------------------------------------------- +instance A.ToJSON Style where + toJSON = A.toJSON . mapMaybe nameForSGR . unStyle + + +-------------------------------------------------------------------------------- +instance A.FromJSON Style where + parseJSON val = do + names <- A.parseJSON val + sgrs <- mapM toSgr names + return $! Style sgrs + where + toSgr name = case M.lookup name sgrsByName of + Just sgr -> return sgr + Nothing -> fail $! + "Unknown style: " ++ show name ++ ". Known styles are: " ++ + intercalate ", " (map show $ M.keys sgrsByName) + + +-------------------------------------------------------------------------------- +nameForSGR :: Ansi.SGR -> Maybe String +nameForSGR (Ansi.SetColor layer intensity color) = Just $ + (\str -> case layer of + Ansi.Foreground -> str + Ansi.Background -> "on" ++ capitalize str) $ + (case intensity of + Ansi.Dull -> "dull" + Ansi.Vivid -> "vivid") ++ + (case color of + Ansi.Black -> "Black" + Ansi.Red -> "Red" + Ansi.Green -> "Green" + Ansi.Yellow -> "Yellow" + Ansi.Blue -> "Blue" + Ansi.Magenta -> "Magenta" + Ansi.Cyan -> "Cyan" + Ansi.White -> "White") + +nameForSGR (Ansi.SetUnderlining Ansi.SingleUnderline) = Just "underline" + +nameForSGR (Ansi.SetConsoleIntensity Ansi.BoldIntensity) = Just "bold" + +nameForSGR _ = Nothing + + +-------------------------------------------------------------------------------- +sgrsByName :: M.Map String Ansi.SGR +sgrsByName = M.fromList + [ (name, sgr) + | sgr <- knownSgrs + , name <- maybeToList (nameForSGR sgr) + ] + where + -- | It doesn't really matter if we generate "too much" SGRs here since + -- 'nameForSGR' will only pick the ones we support. + knownSgrs = + [ Ansi.SetColor l i c + | l <- [minBound .. maxBound] + , i <- [minBound .. maxBound] + , c <- [minBound .. maxBound] + ] ++ + [Ansi.SetUnderlining u | u <- [minBound .. maxBound]] ++ + [Ansi.SetConsoleIntensity c | c <- [minBound .. maxBound]] + + +-------------------------------------------------------------------------------- +newtype SyntaxHighlighting = SyntaxHighlighting + { unSyntaxHighlighting :: M.Map String Style + } deriving (Monoid, Show, A.ToJSON) + + +-------------------------------------------------------------------------------- +instance A.FromJSON SyntaxHighlighting where + parseJSON val = do + styleMap <- A.parseJSON val + forM_ (M.keys styleMap) $ \k -> case nameToTokenType k of + Just _ -> return () + Nothing -> fail $ "Unknown token type: " ++ show k + return (SyntaxHighlighting styleMap) + + +-------------------------------------------------------------------------------- +defaultSyntaxHighlighting :: SyntaxHighlighting +defaultSyntaxHighlighting = mkSyntaxHighlighting + [ (Kate.KeywordTok, dull Ansi.Yellow) + , (Kate.ControlFlowTok, dull Ansi.Yellow) + + , (Kate.DataTypeTok, dull Ansi.Green) + + , (Kate.DecValTok, dull Ansi.Red) + , (Kate.BaseNTok, dull Ansi.Red) + , (Kate.FloatTok, dull Ansi.Red) + , (Kate.ConstantTok, dull Ansi.Red) + , (Kate.CharTok, dull Ansi.Red) + , (Kate.SpecialCharTok, dull Ansi.Red) + , (Kate.StringTok, dull Ansi.Red) + , (Kate.VerbatimStringTok, dull Ansi.Red) + , (Kate.SpecialStringTok, dull Ansi.Red) + + , (Kate.CommentTok, dull Ansi.Blue) + , (Kate.DocumentationTok, dull Ansi.Blue) + , (Kate.AnnotationTok, dull Ansi.Blue) + , (Kate.CommentVarTok, dull Ansi.Blue) + + , (Kate.ImportTok, dull Ansi.Cyan) + , (Kate.OperatorTok, dull Ansi.Cyan) + , (Kate.FunctionTok, dull Ansi.Cyan) + , (Kate.PreprocessorTok, dull Ansi.Cyan) + ] + where + dull c = Style [Ansi.SetColor Ansi.Foreground Ansi.Dull c] + + mkSyntaxHighlighting ls = SyntaxHighlighting $ + M.fromList [(nameForTokenType tt, s) | (tt, s) <- ls] + + +-------------------------------------------------------------------------------- +nameForTokenType :: Kate.TokenType -> String +nameForTokenType = + unCapitalize . dropTok . show + where + unCapitalize (x : xs) = toLower x : xs + unCapitalize xs = xs + + dropTok :: String -> String + dropTok str + | "Tok" `isSuffixOf` str = take (length str - 3) str + | otherwise = str + + +-------------------------------------------------------------------------------- +nameToTokenType :: String -> Maybe Kate.TokenType +nameToTokenType = readMaybe . capitalize . (++ "Tok") + + +-------------------------------------------------------------------------------- +capitalize :: String -> String +capitalize "" = "" +capitalize (x : xs) = toUpper x : xs + + +-------------------------------------------------------------------------------- +syntaxHighlight :: Theme -> Kate.TokenType -> Maybe Style +syntaxHighlight theme tokenType = do + sh <- themeSyntaxHighlighting theme + M.lookup (nameForTokenType tokenType) (unSyntaxHighlighting sh) + + +-------------------------------------------------------------------------------- +$(A.deriveJSON A.dropPrefixOptions ''Theme) diff --git a/src/Text/Pandoc/Extended.hs b/src/Text/Pandoc/Extended.hs new file mode 100644 index 0000000..ab139a9 --- /dev/null +++ b/src/Text/Pandoc/Extended.hs @@ -0,0 +1,50 @@ +-------------------------------------------------------------------------------- +{-# LANGUAGE BangPatterns #-} +{-# LANGUAGE LambdaCase #-} +module Text.Pandoc.Extended + ( module Text.Pandoc + + , plainToPara + , newlineToSpace + , metaToJson + ) where + + +-------------------------------------------------------------------------------- +import qualified Data.Aeson as A +import Data.Data.Extended (grecT) +import qualified Data.Map as M +import Data.Monoid (mempty) +import Text.Pandoc +import Prelude + + +-------------------------------------------------------------------------------- +plainToPara :: [Block] -> [Block] +plainToPara = map $ \case + Plain inlines -> Para inlines + block -> block + + +-------------------------------------------------------------------------------- +newlineToSpace :: [Inline] -> [Inline] +newlineToSpace = grecT $ \case + SoftBreak -> Space + LineBreak -> Space + inline -> inline + + +-------------------------------------------------------------------------------- +-- | Convert Pandoc's internal metadata value format to JSON. This makes +-- parsing some things a bit easier. +metaToJson :: MetaValue -> A.Value +metaToJson (MetaMap m) = A.toJSON $! M.map metaToJson m +metaToJson (MetaList l) = A.toJSON $! map metaToJson l +metaToJson (MetaBool b) = A.toJSON b +metaToJson (MetaString s) = A.toJSON s +metaToJson (MetaInlines i) = + let !t = writeMarkdown def (Pandoc mempty [Plain i]) :: String in + A.toJSON t +metaToJson (MetaBlocks b) = + let !t = writeMarkdown def (Pandoc mempty b) :: String in + A.toJSON t diff --git a/stack.yaml b/stack.yaml new file mode 100644 index 0000000..e3c2c1e --- /dev/null +++ b/stack.yaml @@ -0,0 +1,6 @@ +resolver: lts-7.0 +packages: +- '.' +extra-deps: [] +flags: {} +extra-package-dbs: [] diff --git a/test.sh b/test.sh new file mode 100644 index 0000000..9f8e48d --- /dev/null +++ b/test.sh @@ -0,0 +1,30 @@ +#!/bin/bash +set -o nounset -o errexit -o pipefail + +srcs=$(find tests -type f ! -name '*.dump') +stuff_went_wrong=false + +for src in $srcs; do + expected="$src.dump" + echo -n "Testing $src... " + actual=$(mktemp) + patat --dump --force "$src" >"$actual" + + if [[ $@ == "--fix" ]]; then + cp "$actual" "$expected" + echo 'Fixed' + elif [[ ! -f "$expected" ]]; then + echo "missing file: $expected" + stuff_went_wrong=true + elif [[ "$(cat "$expected")" == "$(cat "$actual")" ]]; then + echo 'OK' + else + echo 'files differ' + diff "$actual" "$expected" || true + stuff_went_wrong=true + fi +done + +if [[ "$stuff_went_wrong" = true ]]; then + exit 1 +fi diff --git a/tests/01.md b/tests/01.md new file mode 100644 index 0000000..2fbdde2 --- /dev/null +++ b/tests/01.md @@ -0,0 +1,14 @@ +--- +title: This is my presentation +author: Jasper Van der Jeugt +... + +# This is a test + +Hello world + +--- + +# This is a second slide + +lololol diff --git a/tests/01.md.dump b/tests/01.md.dump new file mode 100644 index 0000000..1ae41da --- /dev/null +++ b/tests/01.md.dump @@ -0,0 +1,8 @@ +# This is a test + +Hello world + +---------- +# This is a second slide + +lololol diff --git a/tests/02.lhs b/tests/02.lhs new file mode 100644 index 0000000..e61c2d0 --- /dev/null +++ b/tests/02.lhs @@ -0,0 +1,6 @@ +This is how to do _Hello World_ in Haskell: + +> main :: IO () +> main = putStrLn "Hello World!" + +Cool, right? diff --git a/tests/02.lhs.dump b/tests/02.lhs.dump new file mode 100644 index 0000000..594c1bd --- /dev/null +++ b/tests/02.lhs.dump @@ -0,0 +1,8 @@ +This is how to do Hello World in Haskell: + +   +  main :: IO ()  +  main = putStrLn "Hello World!"  +   + +Cool, right? diff --git a/tests/03.md b/tests/03.md new file mode 100644 index 0000000..6b3ae16 --- /dev/null +++ b/tests/03.md @@ -0,0 +1,46 @@ +Inline markups: + +- ~~striked out~~ +- + +--- + +> Some quote + +> Quote with embedded list: +> +> - Hello +> - World + +--- + +- List with an embedded quote: + + > Tu quoque + + Wow rad stuff. + +- Second item in that list. + +--- + +Code with empty line: + + puts "wow" + + puts "amaze" + +--- + +Code in ordered list: + +1. Do you know the coolest codes? + + It's this: + + fire_missiles() + cancel() + + Great + +2. Also `fib` is pretty cool yeah diff --git a/tests/03.md.dump b/tests/03.md.dump new file mode 100644 index 0000000..e8b6b69 --- /dev/null +++ b/tests/03.md.dump @@ -0,0 +1,48 @@ +Inline markups: + + - ~~striked out~~ + - <http://example.com> + +---------- +> Some quote + +> Quote with embedded list: +>  +>  - Hello +>  - World + +---------- + - List with an embedded quote: + + > Tu quoque + + Wow rad stuff. + + - Second item in that list. + + +---------- +Code with empty line: + +   +  puts "wow"  +   +  puts "amaze"  +   + +---------- +Code in ordered list: + +1. Do you know the coolest codes? + + It's this: + +   +  fire_missiles()  +  cancel()  +   + + Great + +2. Also  fib  is pretty cool yeah + diff --git a/tests/deflist.md b/tests/deflist.md new file mode 100644 index 0000000..81aee19 --- /dev/null +++ b/tests/deflist.md @@ -0,0 +1,20 @@ +Term 1 + +: Definition 1 + +Term 2 with *inline markup* + +: Definition 2 + + { some code, part of Definition 2 } + + Third paragraph of definition 2. + +--- + +Term 1 + ~ Definition 1 + +Term 2 + ~ Definition 2a + ~ Definition 2b diff --git a/tests/deflist.md.dump b/tests/deflist.md.dump new file mode 100644 index 0000000..8089fda --- /dev/null +++ b/tests/deflist.md.dump @@ -0,0 +1,24 @@ +Term 1 + +: Definition 1 + +Term 2 with inline markup + +: Definition 2 + +   +  { some code, part of Definition 2 }  +   + + Third paragraph of definition 2. + +---------- +Term 1 + +: Definition 1 + +Term 2 + +: Definition 2a + +: Definition 2b diff --git a/tests/links.md b/tests/links.md new file mode 100644 index 0000000..153f959 --- /dev/null +++ b/tests/links.md @@ -0,0 +1,8 @@ +This is an "automatic link": . + +This is an [inline link](/url), and here's [one with +a title](http://fsf.org "click here for a good time!"). + +Let's talk about [foo][foosite] + +[foosite]: http://foo.com/ diff --git a/tests/links.md.dump b/tests/links.md.dump new file mode 100644 index 0000000..2862e9a --- /dev/null +++ b/tests/links.md.dump @@ -0,0 +1,10 @@ +This is an "automatic link": <https://jaspervdj.be>. + +This is an [inline link], and here's [one with +a title]. + +Let's talk about [foo] + +[inline link](/url) +[one with a title](http://fsf.org "click here for a good time!") +[foo](http://foo.com/) \ No newline at end of file diff --git a/tests/lists.md b/tests/lists.md new file mode 100644 index 0000000..d534704 --- /dev/null +++ b/tests/lists.md @@ -0,0 +1,13 @@ +- This is a nested list. + + * The nested items should have different list markers. + + * I mean, they can be the same, but it doesn't look nice. + + printf("Nested code block!\n") + + * Cool right? + + Definitely super cool + +- One final item diff --git a/tests/lists.md.dump b/tests/lists.md.dump new file mode 100644 index 0000000..1305289 --- /dev/null +++ b/tests/lists.md.dump @@ -0,0 +1,15 @@ + - This is a nested list. + +  * The nested items should have different list markers. + +  * I mean, they can be the same, but it doesn't look nice. + + printf("Nested code block!\n") + +  * Cool right? + + Definitely super cool + + + - One final item + diff --git a/tests/syntax.md b/tests/syntax.md new file mode 100644 index 0000000..f6c803d --- /dev/null +++ b/tests/syntax.md @@ -0,0 +1,14 @@ +--- +patat: + theme: + syntaxHighlighting: + decVal: [bold, onDullRed] +... + +Some simple code: + +```c +int main(int argc, char **argv) { + return 0; +} +``` diff --git a/tests/syntax.md.dump b/tests/syntax.md.dump new file mode 100644 index 0000000..eb4893f --- /dev/null +++ b/tests/syntax.md.dump @@ -0,0 +1,7 @@ +Some simple code: + +   +  int main(int argc, char **argv) {  +  return 0;  +  }  +   diff --git a/tests/tables.md b/tests/tables.md new file mode 100644 index 0000000..fe7d72e --- /dev/null +++ b/tests/tables.md @@ -0,0 +1,48 @@ +# Normal simple table + + Right Left Center Default +------- ------ ---------- ------- + 12 12 12 12 + 123 123 123 123 + 1 1 1 1 + +Table: Demonstration of simple table syntax. + + +# Headerless table + +------- ------ ---------- ------- + 12 12 12 12 + 123 123 123 123 + 1 1 1 1 +------- ------ ---------- ------- + +# Multiline + +------------------------------------------------------------- + Centered Default Right Left + Header Aligned Aligned Aligned +----------- ------- --------------- ------------------------- + First row 12.0 Example of a row that + spans multiple lines. + + Second row 5.0 Here's another one. Note + the blank line between + rows. +------------------------------------------------------------- + +Table: Here's the caption. It, too, may span +multiple lines. + +# Headerless multiline + +----------- ------- --------------- ------------------------- + First row 12.0 Example of a row that + spans multiple lines. + + Second row 5.0 Here's another one. Note + the blank line between + rows. +----------- ------- --------------- ------------------------- + +: Here's a multiline table without headers. diff --git a/tests/tables.md.dump b/tests/tables.md.dump new file mode 100644 index 0000000..0b0a93f --- /dev/null +++ b/tests/tables.md.dump @@ -0,0 +1,48 @@ +# Normal simple table + + Right Left Center Default + ----- ---- ------ ------- + 12 12 12 12  + 123 123 123 123  + 1 1 1 1  + + Table: Demonstration of simple table syntax. + +---------- +# Headerless table + + --- --- --- --- + 12 12 12 12 + 123 123 123 123 + 1 1 1 1  + --- --- --- --- + +---------- +# Multiline + + Centered Default Right Left  + Header Aligned Aligned Aligned  + -------- ------- ------- ------------------------ + First row 12.0 Example of a row that  + spans multiple lines.  +  + Second row 5.0 Here's another one. Note + the blank line between  + rows.  + + Table: Here's the caption. It, too, may span + multiple lines. + +---------- +# Headerless multiline + + ------ --- ---- ------------------------ + First row 12.0 Example of a row that  + spans multiple lines.  +  + Second row 5.0 Here's another one. Note + the blank line between  + rows.  + ------ --- ---- ------------------------ + + Table: Here's a multiline table without headers. diff --git a/tests/themes.md b/tests/themes.md new file mode 100644 index 0000000..6591ece --- /dev/null +++ b/tests/themes.md @@ -0,0 +1,11 @@ +--- +patat: + theme: + bulletListMarkers: '-+' + emph: [onVividRed, underline] +... + +- This is a simple list. + * With _nested_ items. + * One or two. +- The list theming is customized a bit. diff --git a/tests/themes.md.dump b/tests/themes.md.dump new file mode 100644 index 0000000..988214f --- /dev/null +++ b/tests/themes.md.dump @@ -0,0 +1,5 @@ + - This is a simple list. +  + With nested items. +  + One or two. + + - The list theming is customized a bit. diff --git a/tests/wrapping.md b/tests/wrapping.md new file mode 100644 index 0000000..15bc088 --- /dev/null +++ b/tests/wrapping.md @@ -0,0 +1,23 @@ +--- +patat: + wrap: true + columns: 40 +... + +This is a long +sentence over multiple +lines which can be +re-wrapped. + + +This is a super long sentence over a single line which should also be re-wrapped. + + + This is a table and tables should not be wrapped + ------- ------- ---------- ---------- ---------- + 1 2 3 4 5 + 6 7 8 9 10 + +- This is a list +- This list has a really long sentence in it which should also be wrapped with proper indentation +- Another item diff --git a/tests/wrapping.md.dump b/tests/wrapping.md.dump new file mode 100644 index 0000000..e23f9e3 --- /dev/null +++ b/tests/wrapping.md.dump @@ -0,0 +1,17 @@ +This is a long sentence over multiple +lines which can be re-wrapped. + +This is a super long sentence over a +single line which should also be +re-wrapped. + + This is a table and tables should not be wrapped + ------- ------- ---------- ---------- ---------- + 1 2 3 4 5  + 6 7 8 9 10  + + - This is a list + - This list has a really long sentence + in it which should also be wrapped + with proper indentation + - Another item -- 2.30.2